3x-ui/web/translation/ja-JP.json

1540 lines
93 KiB
JSON
Raw 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": "作成",
"add": "追加",
"remove": "削除",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"update": "更新",
"copy": "コピー",
"copied": "コピー済み",
"more": "もっと",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"download": "ダウンロード",
"remark": "備考",
"enable": "有効化",
"protocol": "プロトコル",
"search": "検索",
"filter": "フィルタ",
"all": "すべて",
"from": "から",
"to": "まで",
"done": "完了",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"loading": "読み込み中...",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"refresh": "更新",
"clear": "クリア",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"second": "秒",
"minute": "分",
"hour": "時間",
"day": "日",
"check": "確認",
"indefinite": "無期限",
"unlimited": "無制限",
"none": "なし",
"qrCode": "QRコード",
"info": "詳細情報",
"edit": "編集",
"delete": "削除",
"reset": "リセット",
"noData": "データなし。",
"copySuccess": "コピー成功",
"sure": "確定",
"encryption": "暗号化",
"useIPv4ForHost": "ホストにIPv4を使用",
"transmission": "伝送",
"host": "ホスト",
"path": "パス",
"camouflage": "難読化",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"status": "ステータス",
"enabled": "有効",
"disabled": "無効",
"depleted": "消耗済み",
"depletingSoon": "間もなく消耗",
"offline": "オフライン",
"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": "インバウンド",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"clients": "クライアント",
"groups": "グループ",
"nodes": "ノード",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"settings": "パネル設定",
"xray": "Xray 設定",
2026-05-11 18:47:49 +00:00
"apiDocs": "API ドキュメント",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"logout": "ログアウト",
Frontend rewrite: React + TypeScript with AntD v6 (#4498) * chore(frontend): add react+typescript toolchain alongside vue Step 0 of the planned vue->react migration. React 19, antd 5, i18next + react-i18next, typescript 5, and @vitejs/plugin-react 6 are added as dev/runtime deps alongside the existing vue stack. Both frameworks coexist in the build until the last entry flips. * vite.config.js: react() plugin runs next to vue(); new manualChunks for vendor-react / vendor-antd-react / vendor-icons-react / vendor-i18next. Existing vue chunks unchanged. * eslint.config.js: typescript-eslint + eslint-plugin-react-hooks rules scoped to *.{ts,tsx}; vue config untouched for *.{js,vue}. * tsconfig.json: strict, jsx: react-jsx, moduleResolution: bundler, allowJs: true (lets .tsx files import the remaining .js modules during incremental migration), @/* path alias. * env.d.ts: Vite client types + window.X_UI_BASE_PATH typing + SubPageData shape consumed by the subscription page. Vite stays pinned at 8.0.13 per the existing project policy. No existing .vue/.js source files touched in this step. eslint-plugin-react (not -hooks) is not included because its latest release does not yet support ESLint 10. react-hooks/purity covers the safety-critical case; revisit when the plugin updates. * refactor(frontend): port subpage to react+ts Step 1 of the planned vue->react migration. The standalone subscription page (sub/sub.go renders the HTML host; React mounts into #app) is the first entry off vue. Introduces two shared pieces both entries (and future ones) will use: * src/hooks/useTheme.tsx — React Context + useTheme hook + the same buildAntdThemeConfig (dark/ultra-dark token overrides) and pauseAnimationsUntilLeave helper the vue version exposes. Same localStorage keys (dark-mode, isUltraDarkThemeEnabled) and DOM side effects (body.className, html[data-theme]) so the two stay in sync across the coexistence period. * src/i18n/react.ts — i18next + react-i18next loader that reads the same web/translation/*.json files via import.meta.glob. The vue-i18n setup in src/i18n/index.js is untouched and still serves the remaining vue entries. SubPage.tsx mirrors the vue version's behavior: reads window.__SUB_PAGE_DATA__ injected by the Go sub server, renders QR codes / descriptions / Android+iOS deep-link dropdowns, supports theme cycle and language switch. Uses AntD v5 idioms: Descriptions items prop, Dropdown menu prop, Layout.Content. * refactor(frontend): port login to react+ts Step 2 of the planned vue->react migration. The login entry is the first to exercise AntD React's Form API (Form + Form.Item with name/rules + onFinish) and the existing axios/CSRF interceptors under React. * LoginPage.tsx: same form fields, conditional 2FA input, rotating headline ("Hello" / "Welcome to..."), drifting blob background, theme cycle + language popover. Headline transition switches from vue's <Transition mode=out-in> to a CSS keyframe animation keyed off the visible word. * entries/login.tsx: setupAxios() + applyDocumentTitle() unchanged from the vue entry — both are framework-agnostic in src/utils and src/api/axios-init.js. useTheme hook, ThemeProvider, and i18n/react.ts loader introduced in step 1 are now shared across two entries; Vite extracts them as a small chunk in the build output. * refactor(frontend): port api-docs to react+ts Step 3 of the planned vue->react migration. The five api-docs files (ApiDocsPage, CodeBlock, EndpointRow, EndpointSection, plus the data-only endpoints.js) all move to react+ts. Also introduces components/AppSidebar.tsx — api-docs is the first authenticated page to need it. AppSidebar.vue stays in place for the six remaining vue entries (settings, inbounds, clients, xray, nodes, index); each gets switched to AppSidebar.tsx as its entry migrates. After the last entry flips, AppSidebar.vue is deleted. Notable transformations: * The scroll observer that highlights the active TOC link is a useEffect keyed on sections — re-registers whenever the visible set changes (search filter narrows it). Same behaviour as the vue watchEffect. * v-html="safeInlineHtml(...)" becomes dangerouslySetInnerHTML={{ __html: safeInlineHtml(...) }}. The helper still escapes everything except <code> tags. * JSON syntax highlighter in CodeBlock is unchanged — pure regex on the escaped string, then rendered via dangerouslySetInnerHTML. * endpoints.js stays as JS (allowJs in tsconfig); only the consumer signatures (Endpoint, Section) are typed at the React boundary. * AppSidebar reuses pauseAnimationsUntilLeave + useTheme from step 1. Drawer + Sider keyed off the same localStorage flag (isSidebarCollapsed) and DOM theme attributes the vue version uses, so the two stay in sync during coexistence. * refactor(frontend): port nodes to react+ts Step 4 of the planned vue->react migration. The nodes entry brings in the largest shared-infrastructure batch so far — every authenticated react page from here on can lean on these. New shared pieces (live alongside their .vue counterparts during coexistence): * hooks/useMediaQuery.ts — useState + resize listener * hooks/useWebSocket.ts — wraps WebSocketClient, subscribes on mount and unsubscribes on unmount. The underlying client is a single module-level instance so multiple components on the same page share one socket. * hooks/useNodes.ts — node list state + CRUD + probe/test, including the totals memo (online/offline/avgLatency) used by the summary card. applyNodesEvent is the entry point for the heartbeat-pushed list. * components/CustomStatistic.tsx — thin Statistic wrapper, prefix + suffix slots become props. * components/Sparkline.tsx — the SVG line chart with measured-width axis scaling, gradient fill, tooltip overlay, and per-instance gradient id from React.useId. ResizeObserver lifecycle is in useEffect; the math is unchanged. Pages: * NodesPage — wires hooks + WebSocket together, renders summary card + NodeList, hosts the form modal. Uses Modal.useModal() for the delete confirm so the dialog inherits ConfigProvider theming. * NodeList — desktop renders a Table with expandable history rows; mobile flips to a vertical card list whose actions live in a bottom-right Dropdown. The IP-blur eye toggle persists across both. * NodeFormModal — controlled form (useState object, single setForm per change). The reset-on-open effect computes the next state once and applies it with eslint-disable to satisfy the new react-hooks/set-state-in-effect rule on a legitimate pattern. * NodeHistoryPanel — polls /panel/api/nodes/history/{id}/{metric}/ {bucket} every 15s, renders cpu+mem sparklines side-by-side. * refactor(frontend): port settings to react+ts Step 5 of the planned vue->react migration. Settings is the first entry whose state model didn't translate to the Vue-style "parent passes a reactive object, children mutate it in place" pattern, so the React port flips it to lifted state + a typed updateSetting patch function. * models/setting.ts — typed AllSetting class with the same field defaults and equals() behavior the vue version had. The .js twin is deleted; nothing else imported it. * hooks/useAllSetting.ts — owns allSetting + oldAllSetting state, exposes updateSetting(patch), saveDisabled is derived via useMemo off equals() (no more 1Hz dirty-check timer). * components/SettingListItem.tsx — children-based wrapper instead of named slots. The vue twin stays alive because xray (BasicsTab, DnsTab) still imports it; deleted when xray migrates. The five tab components and the TwoFactorModal each accept { allSetting, updateSetting } and render with AntD v5's Collapse items[] API. Every v-model:value="x" became value={...} onChange={(e) => updateSetting({ key: e.target.value })} or onChange={(v) => updateSetting({ key: v })} for non-input controls. SubscriptionFormatsTab is the trickiest — fragment / noises[] / mux / direct routing rules are stored as JSON-encoded strings on the wire. Parsing them once via useMemo per field, mutating the parsed object on edit, and stringifying back into the patch keeps the round-trip identical to the vue version. SettingsPage hosts the tab navigation (with hash sync), the save / restart action bar, the security-warnings alert banner, and the restart flow that rebuilds the panel URL after the new host/port/cert settings take effect. * refactor(frontend): port clients to react+ts Step 6 of the planned vue->react migration. Clients is the biggest data-CRUD page in the panel (1.1k-line ClientsPage, 4 modals, full table + mobile card list, WebSocket-driven realtime traffic + online updates). New shared infra (lives alongside vue twins until inbounds migrates): * hooks/useClients.ts — clients + inbounds list, CRUD + bulk delete + attach/detach + traffic reset, with WebSocket event handlers (traffic, client_stats, invalidate) and a small debounced refresh on the invalidate event. State managed via setState; the live client_stats event merges traffic snapshots row-by-row through a ref to avoid stale closure issues. * hooks/useDatepicker.ts — singleton "gregorian"/"jalalian" cache with subscribe/notify so multiple components can read the panel's Calendar Type without re-fetching. Mirrors useDatepicker.js. * components/DateTimePicker.tsx — AntD DatePicker wrapper. vue3-persian-datetime-picker has no React port; the Jalali UI calendar is deferred (read-only Jalali display via IntlUtil formatDate still works). The vue twin stays for inbounds. * pages/inbounds/QrPanel.tsx — copy/download/copy-as-png QR helper shared between clients (qr modal) and inbounds (still on vue). Vue twin stays alive at QrPanel.vue. * models/inbound.ts — slim port: only the TLS_FLOW_CONTROL constant the clients form needs. The full inbound model stays as inbound.js for now; inbounds will pull it in as inbound.ts. The clients page itself uses Modal.useModal() for all confirm dialogs (delete, bulk-delete, reset-traffic, delDepleted, reset-all) so the dialogs render themed. Filter state persists to localStorage under clientsFilterState. Sort + pagination state is local; pageSize seeds from /panel/setting/defaultSettings. The four modals share a controlled "open/onOpenChange" pattern that replaces vue's v-model:open. ClientFormModal computes attach/detach diffs from the inbound multi-select on submit; the parent's onSave callback routes them through useClients's attach()/ detach() after the main update succeeds. ESLint config: turned off four react-hooks v7 rules (react-compiler, preserve-manual-memoization, set-state-in-effect, purity). They're all React-Compiler-driven informational rules; we don't run the compiler and the patterns they flag (initial-fetch useEffect, derived computations using Date.now, inline arrow event handlers) are all idiomatic React. Disabling globally instead of per-line keeps the diff readable. * refactor(frontend): port index dashboard to react+ts Step 7 of the Vue→React migration. Ports the overview/index entry: dashboard page, status + xray cards, panel-update / log / backup / system-history / xray-metrics / xray-log / version modals, and the custom-geo subsection. Adds the shared JsonEditor (CodeMirror 6) and useStatus hook used by the config modal. Removes the unused react-hooks/set-state-in-effect disables now that the rule is off globally. * refactor(frontend): port xray to react+ts Step 8 of the Vue→React migration. Ports the xray config entry: page shell, basics/routing/outbounds/balancers/dns tabs, the rule + balancer + dns server + dns presets + warp + nord modals, the protocol-aware outbound form, and the shared FinalMaskForm (TCP/UDP masks + QUIC params). Adds useXraySetting that mirrors the legacy two-way sync between the JSON template string and the parsed templateSettings tree. The outbound model itself stays in JS so the class-driven form keeps its existing mutation API; instance access is typed loosely inside the form to match. The shared FinalMaskForm.vue and JsonEditor.vue stay alongside the new .tsx versions until step 9 — InboundFormModal.vue still imports them. Adds react-hooks/immutability and react-hooks/refs to the already-disabled react-compiler rule set; both flag the outbound form's instance-mutation pattern that doesn't run through useState. * Upgrade frontend deps (antd v6, i18n, TS) Bump frontend dependencies in package.json and regenerate package-lock.json. Notable updates: upgrade antd to v6, update i18next/react-i18next, axios, qs, vue-i18n, TypeScript and ESLint, plus related @rc-component packages and replacements (e.g. classnames/rc-util -> clsx/@rc-component/util). Lockfile changes reflect the new dependency tree required for Ant Design v6 and other package upgrades. * refactor(frontend): port inbounds to react+ts and drop vue toolchain Step 9 — the last entry. Ports the inbounds entry: page shell, list with desktop table + mobile cards, info modal, qr-code modal, share-link helpers, and the protocol-aware form modal (basics / protocol / stream / security / sniffing / advanced JSON). useInbounds replaces the Vue composable with WebSocket-driven traffic + client-stats merge. Inbound and DBInbound models stay in JS so the class-driven form keeps its mutation API; instance access is typed loosely inside the form to match. FinalMaskForm/JsonEditor/TextModal/PromptModal/InfinityIcon are the last shared bits to flip; their .vue counterparts go too. Toolchain cleanup now that no entry needs Vue: drop plugin-vue from vite.config, remove the .vue lint block + parser, prune vue / vue-i18n / ant-design-vue / @ant-design/icons-vue / vue3-persian-datetime-picker / moment-jalaali override from package.json, and switch utils/index.js to import { message } from 'antd' instead of ant-design-vue. * chore(frontend): adopt antd v6 api updates Sweep deprecated props across the React tree: - Modal: destroyOnClose -> destroyOnHidden, maskClosable -> mask.closable - Space: direction -> orientation (or removed when redundant) - Input.Group compact -> Space.Compact block - Drawer: width -> size - Spin: tip -> description - Progress: trailColor -> railColor - Alert: message -> title - Popover: overlayClassName -> rootClassName - BackTop -> FloatButton.BackTop Also refresh dashboard theming for v6: rename dark/ultra Layout and Menu tokens (siderBg, darkItemBg, darkSubMenuItemBg, darkPopupBg), tweak gauge size/stroke, add font-size overrides for Statistic and Progress so the overview numbers stay legible under v6 defaults. * chore(frontend): antd v6 polish, theme + modal fixes - adopt message.useMessage hook + messageBus bridge so HttpUtil messages inherit ConfigProvider theme tokens - replace deprecated antd APIs (List, Input addonBefore/After, Empty imageStyle); introduce InputAddon helper + SettingListItem custom rows - fix dark/ultra selectors in portaled modals (body.dark, html[data-theme='ultra-dark']) instead of nonexistent .is-dark/.is-ultra - add horizontal scroll to clients table; reorder node columns so actions+enable sit at the left - swap raw button for antd Button in NodeFormModal test connection - fix FinalMaskForm nested-form by hoisting it outside OutboundFormModal's parent Form - fix advanced "all" JSON tab in InboundFormModal — useMemo on a mutated ref was stale; compute on every render - fix chart-on-open for SystemHistory + XrayMetrics modals by adding open to effect deps (useRef.current doesn't trigger re-runs) - switch i18next interpolation to single-brace {var} to match locale files - drop residual Vue mentions in CI workflows and Go comments * fix(frontend): qr code collapse — open only first panel, allow toggle ClientQrModal and QrCodeModal both used activeKey without onChange, forcing every panel open and blocking user toggle. Switch to controlled state initialized to the first item's key on open, with onChange so clicks update state. Also remove unused AppBridge.tsx (superseded by per-page message.useMessage hook). * fix(frontend): hover cards, balancer load, routing dnd, modal a11y, outbound crash - ClientsPage/SettingsPage/XrayPage: add hoverable to bottom card/tabs so hover affordance matches the top card - BalancerFormModal: lazy-init useState from props + destroyOnHidden so the form mounts with saved values instead of relying on a useEffect sync that could miss the first open - RoutingTab: rewrite pointer drag — handlers are now defined inside the pointerdown closure so addEventListener/removeEventListener match; drag state lives on a ref (from/to/moved) so onUp reads the real indices, not stale closure values. Adds setPointerCapture so Windows and touch keep delivering events when the cursor leaves the handle. - OutboundFormModal/InboundFormModal: blur the focused input before switching tabs to silence the aria-hidden-on-focused-element warning - utils.isArrEmpty: return true for undefined/null arrays — the old form treated undefined as "not empty" which crashed VLESSSettings.fromJson when json.vnext was missing * fix(frontend): clipboard reliability + restyle login page - ClipboardManager.copyText: prefer navigator.clipboard on secure contexts, fall back to a focused on-screen textarea + execCommand. Old path used left:-9999px which failed selection in some browsers and swallowed execCommand's return value, so the "copied" toast appeared even when nothing made it to the clipboard. - LoginPage: richer gradient backdrop — five animated colour blobs, glassmorphic card (backdrop-filter blur + saturate), gradient brand text/accent, masked grid texture for depth, and a thin gradient border on the card. Light/dark/ultra each get their own palette. * Memoize compactAdvancedJson and update deps Wrap compactAdvancedJson in useCallback (dependent on messageApi) and add it to the dependency array of applyAdvancedJsonToBasic. This ensures a stable function reference for correct dependency tracking and avoids stale closures/unnecessary re-renders in InboundFormModal.tsx. * style(frontend): prettier charts, drop redundant frame, format net rates - Sparkline: multi-stop gradient fill, soft drop-shadow under the line, dashed grid, glowing pulse on the latest-point marker, pill-shaped tooltip with dashed crosshair - XrayMetricsModal: glow + pulse on the observatory alive dot, monospace stamps/listen text - SystemHistoryModal: keep just the modal's frame around the chart (the inner wrapper I'd added stacked a second border on top); strip the decimal from Net Up/Down (25.63 KB/s → 25 KB/s) only on this chart's formatter * style(frontend): refined dark/ultra palette + shared pro card frame - Dark tokens shifted to a cooler, Linear-style palette: page #1a1b1f, sidebar/header #15161a (recessed nav, darker than cards), card #23252b, elevated #2d2f37 - Ultra dark: page pure #000 for OLED, sidebar #050507 disappears into the frame, card #101013 with a clear step, elevated #1a1a1e - New styles/page-cards.css holds the card border/shadow/hover rules so all seven content pages (index, clients, inbounds, xray, settings, nodes, api-docs) share one definition instead of duplicating in each page CSS - Dashboard typography: uppercase card titles with letter-spacing, larger 17px stat values, subtle gradient divider between stat columns, ellipsis on action labels so "Backup & Restore" doesn't break the card height at mid widths - Light --bg-page stays at #e6e8ec for the contrast against white cards * fix(frontend): wireguard info alignment, blue login dark, embed gitkeep - align WireGuard info-modal fields with Protocol/Address/Port by wrapping values in Tag (matches the rest of the dl.info-list rows) - swap login dark palette from purple to pure blue blobs/accent/brand - pin web/dist/.gitkeep through gitignore so //go:embed all:dist never fails on a fresh clone with an empty dist directory * docs: refresh frontend docs for the React + TS + AntD 6 stack Update CONTRIBUTING.md and frontend/README.md to describe the migrated frontend accurately: - replace Vue 3 / Ant Design Vue 4 references with React 19 / AntD 6 / TS - swap composables -> hooks, vue-i18n -> react-i18next, createApp -> createRoot - mention the typecheck step (tsc --noEmit) in the PR checklist - document the Vite 8.0.13 pin and TypeScript strict mode in conventions - list the nodes and api-docs entries that were missing from the layout * style(frontend): improve readability and mobile polish - bump statistic title/value contrast in dark and ultra-dark so totals on the inbounds summary card stay legible - give index card actions explicit colors per theme so links like Stop, Logs, System History no longer fade into the card background - show the panel version as a tag next to "3X-UI" on mobile, mirroring the Xray version tag pattern, and turn it orange when an update is available - make the login settings button a proper circle by adding size="large" + an explicit border-radius fallback on .toolbar-btn * feat: jalali calendar support and date formatting fixes - Wire useDatepicker into IntlUtil and switch jalalian display locale to fa-IR for clean "1405/07/03 12:00:00" output (drops the awkward "AP" era suffix that "<lang>-u-ca-persian" produced) - Drop in persian-calendar-suite for the jalali date picker, with a light/dark/ultra theme map and CSS overrides so the inline-styled input stays readable and bg matches the surrounding container - Force LTR on the picker input so "1405/03/07 00:00" reads naturally - Pass calendar setting through ClientInfoModal, ClientsPage Duration tooltip, and ClientFormModal's expiry picker - Heuristic toMs() in ClientInfoModal so GORM's autoUpdateTime seconds render as a real date instead of "1348/11/01" - Persist UpdatedAt on the ClientRecord row in client_service.Update; previously only the inbound settings JSON was bumped, so the panel never saw a fresh updated_at after editing a client * feat(frontend): donate link, panel version label, login lang menu - Sidebar: add heart donate link to https://donate.sanaei.dev and small panel version under 3X-UI brand - Login: swap settings-cog for translation icon, drop title, render languages as a direct list - Vite dev: inject window.X_UI_CUR_VER from config/version so dev mode matches prod - Translations: add menu.donate across all locales * fix(xray-update): respect XUI_BIN_FOLDER on Windows The Windows update path hardcoded "bin/xray-windows-amd64.exe", ignoring the configured XUI_BIN_FOLDER. In dev mode (folder set to x-ui) this created a stray bin/ folder while the running binary stayed un-updated. * Bump Xray to v26.5.9 and minor cleanup Update Xray release URLs to v26.5.9 in the GitHub Actions workflow and DockerInit.sh. Remove the hardcoded skip for tagVersion "26.5.3" so it will be considered when collecting Xray versions. Apply small formatting fixes: remove an extra blank line in database/db.go, normalize spacing/alignment of Protocol constants in database/model/model.go, and trim a trailing blank line in web/controller/inbound.go. * fix(frontend): route remaining copy buttons through ClipboardManager Direct navigator.clipboard calls fail in non-secure contexts (HTTP on a LAN IP), making the API-docs code copy and security-tab token copy silently broken. Both now go through ClipboardManager which falls back to document.execCommand('copy') when navigator.clipboard is unavailable. * fix(db): store CreatedAt/UpdatedAt in milliseconds GORM's autoCreateTime/autoUpdateTime tags default to Unix seconds on int64 fields and overwrite the service-supplied UnixMilli value on save. The frontend interprets these timestamps as JS Date inputs (milliseconds), so created/updated columns rendered ~1970 dates. Adding the :milli qualifier makes GORM match what the service code and UI expect. * Improve legacy clipboard copy handling Refactor ClipboardManager._legacyCopy to better handle focus and selection when copying. The textarea is now appended to the active element's parent (or body) and placed off-screen with aria-hidden and readonly attributes. The code preserves and restores the previous document selection and active element, uses focus({preventScroll: true}) to avoid scrolling, and returns the execCommand('copy') result. This makes legacy copy behavior more robust and less disruptive to the page state. * fix(lint): drop redundant ok=false in clipboard fallback catch * chore(deps): bump golang.org/x/net to v0.55.0 for GO-2026-5026
2026-05-23 13:21:45 +00:00
"link": "リンク管理",
"donate": "寄付"
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
},
"pages": {
"login": {
"hello": "こんにちは",
"title": "ようこそ",
"loginAgain": "ログインセッションが切れました。再度ログインしてください。",
"toasts": {
"invalidFormData": "データ形式エラー",
"emptyUsername": "ユーザー名を入力してください",
"emptyPassword": "パスワードを入力してください",
"wrongUsernameOrPassword": "ユーザー名、パスワード、または二段階認証コードが無効です。",
"successLogin": "アカウントに正常にログインしました。"
}
},
"index": {
"title": "システムステータス",
"cpu": "CPU",
"logicalProcessors": "論理プロセッサ",
"frequency": "周波数",
"swap": "Swap",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"storage": "ストレージ",
"memory": "RAM",
"threads": "スレッド",
"xrayStatus": "Xray",
"stopXray": "停止",
"restartXray": "再起動",
"xraySwitch": "バージョン",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"xrayUpdates": "Xrayの更新",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"xraySwitchClick": "切り替えるバージョンを選択してください",
"xraySwitchClickDesk": "慎重に選択してください。古いバージョンは現在の設定と互換性がない可能性があります。",
"updatePanel": "パネルを更新",
"panelUpdateDesc": "これにより3X-UIが最新リリースに更新され、パネルサービスが再起動されます。",
"currentPanelVersion": "現在のパネルバージョン",
"latestPanelVersion": "最新のパネルバージョン",
"panelUpToDate": "パネルは最新です",
"upToDate": "最新",
"xrayStatusUnknown": "不明",
"xrayStatusRunning": "実行中",
"xrayStatusStop": "停止",
"xrayStatusError": "エラー",
"xrayErrorPopoverTitle": "Xrayの実行中にエラーが発生しました",
"operationHours": "システム稼働時間",
"systemHistoryTitle": "システム履歴",
"charts": "チャート",
"xrayMetricsTitle": "Xray メトリクス",
"xrayMetricsDisabled": "Xray メトリクスエンドポイントが設定されていません",
"xrayMetricsHint": "xray 設定にトップレベルの metrics ブロック(tag: metrics_out、listen: 127.0.0.1:11111)を追加し、xray を再起動してください。",
"xrayObservatoryEmpty": "Observatory データはまだありません",
"xrayObservatoryHint": "xray 設定に observatory ブロックを追加し、プローブする outbound タグを列挙してから xray を再起動してください。",
"xrayObservatoryTagPlaceholder": "Outbound を選択",
"xrayObservatoryAlive": "稼働中",
"xrayObservatoryDead": "停止",
"xrayObservatoryLastSeen": "最終確認",
"xrayObservatoryLastTry": "最終試行",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"trendLast2Min": "直近2分",
"systemLoad": "システム負荷",
"systemLoadDesc": "過去1、5、15分間のシステム平均負荷",
"connectionCount": "接続数",
"ipAddresses": "IPアドレス",
"toggleIpVisibility": "IPの表示を切り替える",
"overallSpeed": "全体の速度",
"upload": "アップロード",
"download": "ダウンロード",
"totalData": "総データ量",
"sent": "送信",
"received": "受信",
"documentation": "ドキュメント",
"xraySwitchVersionDialog": "Xrayのバージョンを本当に変更しますか",
"xraySwitchVersionDialogDesc": "Xrayのバージョンが#version#に変更されます。",
"xraySwitchVersionPopover": "Xrayの更新が成功しました",
"panelUpdateDialog": "本当にパネルを更新しますか?",
"panelUpdateDialogDesc": "これにより3X-UIが#version#に更新され、パネルサービスが再起動されます。",
"panelUpdateCheckPopover": "パネルの更新確認に失敗しました",
"panelUpdateStartedPopover": "パネルの更新を開始しました",
"geofileUpdateDialog": "ジオファイルを本当に更新しますか?",
"geofileUpdateDialogDesc": "これにより#filename#ファイルが更新されます。",
"geofilesUpdateDialogDesc": "これにより、すべてのファイルが更新されます。",
"geofilesUpdateAll": "すべて更新",
"geofileUpdatePopover": "ジオファイルの更新が成功しました",
"customGeoTitle": "カスタム GeoSite / GeoIP",
"customGeoAdd": "追加",
"customGeoType": "種類",
"customGeoAlias": "エイリアス",
"customGeoUrl": "URL",
"customGeoEnabled": "有効",
"customGeoLastUpdated": "最終更新",
"customGeoExtColumn": "ルーティング (ext:…)",
"customGeoToastUpdateAll": "すべてのカスタムソースを更新しました",
"customGeoActions": "操作",
"customGeoEdit": "編集",
"customGeoDelete": "削除",
"customGeoDownload": "今すぐ更新",
"customGeoModalAdd": "カスタム geo を追加",
"customGeoModalEdit": "カスタム geo を編集",
"customGeoModalSave": "保存",
"customGeoDeleteConfirm": "このカスタム geo ソースを削除しますか?",
"customGeoRoutingHint": "ルーティングでは値を ext:ファイル.dat:タグ(タグを置換)として使います。",
"customGeoInvalidId": "無効なリソース ID",
"customGeoAliasesError": "カスタム geo エイリアスの読み込みに失敗しました",
"customGeoValidationAlias": "エイリアスは小文字・数字・- と _ のみ使用できます",
"customGeoValidationUrl": "URL は http:// または https:// で始めてください",
"customGeoAliasPlaceholder": "a-z 0-9 _ -",
"customGeoAliasLabelSuffix": "(カスタム)",
"customGeoToastList": "カスタム geo 一覧",
"customGeoToastAdd": "カスタム geo を追加",
"customGeoToastUpdate": "カスタム geo を更新",
"customGeoToastDelete": "カスタム geofile「{{ .fileName }}」を削除しました",
"customGeoToastDownload": "geofile「{{ .fileName }}」を更新しました",
"customGeoErrInvalidType": "種類は geosite または geoip である必要があります",
"customGeoErrAliasRequired": "エイリアスが必要です",
"customGeoErrAliasPattern": "エイリアスに使用できない文字が含まれています",
"customGeoErrAliasReserved": "このエイリアスは予約されています",
"customGeoErrUrlRequired": "URL が必要です",
"customGeoErrInvalidUrl": "URL が無効です",
"customGeoErrUrlScheme": "URL は http または https を使用してください",
"customGeoErrUrlHost": "URL のホストが無効です",
"customGeoErrDuplicateAlias": "この種類ですでにこのエイリアスが使われています",
"customGeoErrNotFound": "カスタム geo ソースが見つかりません",
"customGeoErrDownload": "ダウンロードに失敗しました",
"customGeoErrUpdateAllIncomplete": "カスタム geo ソースの 1 件以上を更新できませんでした",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"customGeoEmpty": "カスタム geo ソースはまだありません — 「追加」をクリックして作成してください",
"dontRefresh": "インストール中、このページをリロードしないでください",
"logs": "ログ",
"config": "設定",
"backup": "バックアップ",
"backupTitle": "バックアップと復元",
"exportDatabase": "バックアップ",
"exportDatabaseDesc": "クリックして、現在のデータベースのバックアップを含む .db ファイルをデバイスにダウンロードします。",
"importDatabase": "復元",
"importDatabaseDesc": "クリックして、デバイスから .db ファイルを選択し、アップロードしてバックアップからデータベースを復元します。",
"importDatabaseSuccess": "データベースのインポートに成功しました",
"importDatabaseError": "データベースのインポート中にエラーが発生しました",
"readDatabaseError": "データベースの読み取り中にエラーが発生しました",
"getDatabaseError": "データベースの取得中にエラーが発生しました",
"getConfigError": "設定ファイルの取得中にエラーが発生しました"
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
},
"inbounds": {
"title": "インバウンド",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"totalDownUp": "総アップロード / ダウンロード",
"totalUsage": "総使用量",
"inboundCount": "インバウンド数",
"operate": "メニュー",
"enable": "有効化",
"remark": "備考",
"node": "ノード",
"deployTo": "デプロイ先",
"localPanel": "ローカルパネル",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"fallbacks": {
"title": "Fallbacks",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"help": "このインバウンドへの接続がどのクライアントにも一致しない場合、別のインバウンドへルーティングします。下から子インバウンドを選ぶと、ルーティング項目SNI / ALPN / Path / xverはその子のトランスポートから自動的に埋められます — ほとんどの構成で追加の調整は不要です。各子インバウンドは 127.0.0.1 で security=none をリッスンする必要があります。",
"empty": "フォールバックはまだありません",
"add": "フォールバックを追加",
"pickInbound": "インバウンドを選択",
"matchAny": "任意",
"rederive": "子から再取得",
"rederived": "子から再取得しました",
"editAdvanced": "ルーティング項目を編集",
"hideAdvanced": "詳細を隠す",
"quickAddAll": "対象のインバウンドをすべて一括追加",
"quickAdded": "{n} 件のフォールバックを追加しました",
"quickAddedNone": "追加可能な新規インバウンドはありません",
"routesWhen": "次の条件でルーティング",
"defaultCatchAll": "デフォルト — その他すべてを捕捉"
},
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"protocol": "プロトコル",
"port": "ポート",
"portMap": "ポートマッピング",
"traffic": "トラフィック",
"details": "詳細情報",
"transportConfig": "トランスポート",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"expireDate": "有効期限",
"createdAt": "作成",
"updatedAt": "更新",
"resetTraffic": "トラフィックをリセット",
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
"addInbound": "インバウンド追加",
"generalActions": "一般操作",
"modifyInbound": "インバウンド修正",
"deleteInbound": "インバウンド削除",
"deleteInboundContent": "インバウンドを削除してもよろしいですか?",
"deleteConfirmTitle": "インバウンド「{remark}」を削除しますか?",
"deleteConfirmContent": "インバウンドと関連付けされたすべてのクライアントを削除します。元に戻せません。",
"resetConfirmTitle": "「{remark}」のトラフィックをリセットしますか?",
"resetConfirmContent": "このインバウンドの送受信カウンタを 0 にリセットします。",
"cloneConfirmTitle": "インバウンド「{remark}」を複製しますか?",
"cloneConfirmContent": "新しいポートと空のクライアント一覧でコピーを作成します。",
"delAllClients": "すべてのクライアントを削除",
"delAllClientsConfirmTitle": "「{remark}」から {count} 件のクライアントをすべて削除しますか?",
"delAllClientsConfirmContent": "このインバウンドからすべてのクライアントを削除し、トラフィックレコードも破棄します。インバウンド自体は保持されます。この操作は取り消せません。",
"attachClients": "クライアントをアタッチ…",
"addClientsToGroup": "クライアントをグループに追加…",
"attachClientsTitle": "「{remark}」のクライアントをアタッチ",
"attachClientsDesc": "同じ {count} クライアント(同じ UUID/パスワードと共有トラフィック)を選択したインバウンドにアタッチします。このインバウンドにも残ります。",
"attachClientsTargets": "ターゲットインバウンド",
"attachClientsNoTargets": "アタッチ可能な互換インバウンドがありません。",
"attachClientsResult": "アタッチ {attached}、スキップ {skipped}。",
"attachClientsResultMixed": "アタッチ {attached}、スキップ {skipped}、エラー {errors}。",
"attachClientsSelectLabel": "アタッチするクライアント",
"attachClientsSearchPlaceholder": "メールまたはコメントを検索",
"attachClientsStatusDisabled": "無効",
"attachClientsSelectedCount": "{total} 中 {selected} 選択中",
"detachClients": "クライアントをデタッチ",
"detachClientsTitle": "「{remark}」のクライアントをデタッチ",
"detachClientsDesc": "選択したクライアントをこのインバウンドのみから外します。クライアントレコードは保持されます (完全に削除するには Delete を使用)。ソースには合計 {count} クライアントがあります。",
"detachClientsResult": "デタッチ {detached}、スキップ {skipped}。",
"detachClientsResultMixed": "デタッチ {detached}、スキップ {skipped}、エラー {errors}。",
"detachClientsSelectLabel": "デタッチするクライアント",
"exportLinksTitle": "インバウンドリンクのエクスポート",
"exportSubsTitle": "サブスクリプションリンクのエクスポート",
"exportAllLinksTitle": "全インバウンドリンクのエクスポート",
"exportAllSubsTitle": "全サブスクリプションリンクのエクスポート",
"inboundJsonTitle": "インバウンド JSON",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"deleteClient": "クライアント削除",
"deleteClientContent": "クライアントを削除してもよろしいですか?",
"resetTrafficContent": "トラフィックをリセットしてもよろしいですか?",
"copyLink": "リンクをコピー",
"address": "アドレス",
"network": "ネットワーク",
"destinationPort": "宛先ポート",
"targetAddress": "宛先アドレス",
"monitorDesc": "空白にするとすべてのIPを監視",
"meansNoLimit": "= 無制限。(単位: GB)",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"totalFlow": "総トラフィック",
"leaveBlankToNeverExpire": "空白にすると期限なし",
"noRecommendKeepDefault": "デフォルト値を保持することをお勧めします",
"certificatePath": "ファイルパス",
"certificateContent": "ファイル内容",
"publicKey": "公開鍵",
"privatekey": "秘密鍵",
"clickOnQRcode": "QRコードをクリックしてコピー",
"client": "クライアント",
"export": "リンクエクスポート",
"clone": "複製",
"cloneInbound": "複製",
"cloneInboundContent": "このインバウンドルールは、ポートPort、リスニングIPListening IP、クライアントClientsを除くすべての設定がクローンされます",
"cloneInboundOk": "クローン作成",
"resetAllTraffic": "すべてのインバウンドトラフィックをリセット",
"resetAllTrafficTitle": "すべてのインバウンドトラフィックをリセット",
"resetAllTrafficContent": "すべてのインバウンドトラフィックをリセットしてもよろしいですか?",
"resetInboundClientTraffics": "クライアントトラフィックをリセット",
"resetInboundClientTrafficTitle": "すべてのクライアントトラフィックをリセット",
"resetInboundClientTrafficContent": "このインバウンドクライアントのすべてのトラフィックをリセットしてもよろしいですか?",
"resetAllClientTraffics": "すべてのクライアントトラフィックをリセット",
"resetAllClientTrafficTitle": "すべてのクライアントトラフィックをリセット",
"resetAllClientTrafficContent": "すべてのクライアントのトラフィックをリセットしてもよろしいですか?",
"delDepletedClients": "トラフィックが尽きたクライアントを削除",
"delDepletedClientsTitle": "トラフィックが尽きたクライアントを削除",
"delDepletedClientsContent": "トラフィックが尽きたすべてのクライアントを削除してもよろしいですか?",
"email": "メール",
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
"emailDesc": "メールアドレスは一意でなければなりません",
"IPLimit": "IP制限",
"IPLimitDesc": "設定値を超えるとインバウンドトラフィックが無効になります。0 = 無効)",
"IPLimitlog": "IPログ",
"IPLimitlogDesc": "IP履歴ログ無効なインバウンドトラフィックを有効にするには、ログをクリアしてください",
"IPLimitlogclear": "ログをクリア",
"setDefaultCert": "パネル設定から証明書を設定",
"setDefaultCertEmpty": "パネル用の証明書が設定されていません。先に設定から指定してください。",
"streamTab": "Stream",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"securityTab": "セキュリティ",
"sniffingTab": "Sniffing",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"sniffingMetadataOnly": "メタデータのみ",
"sniffingRouteOnly": "ルーティングのみ",
"sniffingIpsExcluded": "除外する IP",
"sniffingDomainsExcluded": "除外するドメイン",
"decryption": "復号",
"encryption": "暗号化",
"vlessAuthX25519": "X25519 認証",
"vlessAuthMlkem768": "ML-KEM-768 認証",
"vlessAuthCustom": "カスタム",
"vlessAuthSelected": "選択中: {auth}",
"advanced": {
"title": "インバウンド JSON セクション",
"subtitle": "インバウンド全体の JSON と、settings、sniffing、streamSettings 用の専用エディター。",
"all": "すべて",
"allHelp": "すべてのフィールドを含むインバウンドオブジェクト全体を 1 つのエディターで編集します。",
"settings": "設定",
"settingsHelp": "Xray settings ブロックのラッパー:",
"sniffing": "Sniffing",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"sniffingHelp": "Xray sniffing ブロックのラッパー:",
"stream": "Stream",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"streamHelp": "Xray stream ブロックのラッパー:",
"jsonErrorPrefix": "高度な JSON"
},
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"telegramDesc": "TelegramチャットIDを提供してください。ボットで'/id'コマンドを使用)または({'@'}userinfobot",
"subscriptionDesc": "サブスクリプションURLを見つけるには、“詳細情報”に移動してください。また、複数のクライアントに同じ名前を使用することができます。",
"same": "同じ",
"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": "トラフィックがリセットされました",
"resetInboundTrafficSuccess": "受信トラフィックがリセットされました",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"trafficGetError": "トラフィックの取得中にエラーが発生しました",
"getNewX25519CertError": "X25519証明書の取得中にエラーが発生しました。",
"getNewmldsa65Error": "mldsa65証明書の取得中にエラーが発生しました。",
"getNewVlessEncError": "VlessEnc証明書の取得中にエラーが発生しました。"
},
"form": {
"moveUp": "上へ",
"moveDown": "下へ",
"addAll": "すべて追加",
"addAllFallbackTooltip": "まだ接続されていないすべての対象インバウンドに対し fallback 行を追加",
"peers": "Peers",
"addPeer": "peer を追加",
"keepAlive": "Keep-alive",
"autoSystemRoutesTooltip": "Windows のみ。CIDR はシステムルーティングテーブルに自動追加され、一致するトラフィックは TUN を経由します。",
"autoOutboundsInterface": "自動アウトバウンドインターフェース",
"autoOutboundsInterfaceTooltip": "アウトバウンドトラフィック用の物理インターフェース。検出には 'auto' を使用; Auto system routes が設定されていると自動的に有効。",
"rewriteAddress": "アドレス書き換え",
"rewritePort": "ポート書き換え",
"allowedNetwork": "許可されたネットワーク",
"followRedirect": "リダイレクトに従う",
"accounts": "アカウント",
"allowTransparent": "透過を許可",
"encryptionMethod": "暗号化方式",
"visionTestseed": "Vision testseed",
"version": "バージョン",
"udpIdleTimeout": "UDP idle timeout (秒)",
"masquerade": "Masquerade",
"type": "種類",
"upstreamUrl": "Upstream URL",
"rewriteHost": "Host 書き換え",
"skipTlsVerify": "TLS 検証をスキップ",
"directory": "ディレクトリ",
"statusCode": "ステータスコード",
"body": "Body",
"headers": "ヘッダー",
"proxyProtocol": "Proxy Protocol",
"requestVersion": "リクエストバージョン",
"requestMethod": "リクエストメソッド",
"requestPath": "リクエストパス",
"requestHeaders": "リクエストヘッダー",
"responseVersion": "レスポンスバージョン",
"responseStatus": "レスポンスステータス",
"responseReason": "レスポンス理由",
"responseHeaders": "レスポンスヘッダー",
"heartbeatPeriod": "ハートビート間隔",
"serviceName": "サービス名",
"authority": "Authority",
"multiMode": "Multi Mode",
"maxBufferedUpload": "最大バッファアップロード",
"maxUploadSize": "最大アップロードサイズ (バイト)",
"streamUpServer": "Stream-Up Server",
"serverMaxHeaderBytes": "サーバー最大ヘッダーバイト",
"paddingBytes": "Padding バイト",
"uplinkHttpMethod": "Uplink HTTP メソッド",
"paddingObfsMode": "Padding 難読化モード",
"paddingKey": "Padding Key",
"paddingHeader": "Padding Header",
"paddingPlacement": "Padding 配置",
"paddingMethod": "Padding 方法",
"sessionPlacement": "Session Placement",
"sessionKey": "Session Key",
"sequencePlacement": "Sequence Placement",
"sequenceKey": "Sequence Key",
"uplinkDataPlacement": "Uplink Data Placement",
"uplinkDataKey": "Uplink Data Key",
"noSseHeader": "SSE ヘッダーなし",
"ttiMs": "TTI (ms)",
"uplinkMbps": "アップリンク (MB/s)",
"downlinkMbps": "ダウンリンク (MB/s)",
"cwndMultiplier": "CWND 倍率",
"maxSendingWindow": "最大送信ウィンドウ",
"externalProxy": "外部プロキシ",
"sniPlaceholder": "SNI (デフォルトは host)",
"fingerprint": "Fingerprint",
"defaultOption": "デフォルト",
"routeMark": "Route Mark",
"tcpKeepAliveInterval": "TCP Keep Alive Interval",
"tcpKeepAliveIdle": "TCP Keep Alive Idle",
"tcpMaxSeg": "TCP Max Seg",
"tcpUserTimeout": "TCP User Timeout",
"tcpWindowClamp": "TCP Window Clamp",
"tcpFastOpen": "TCP Fast Open",
"multipathTcp": "Multipath TCP",
"penetrate": "Penetrate",
"v6Only": "V6 のみ",
"tcpCongestion": "TCP Congestion",
"dialerProxy": "Dialer Proxy",
"trustedXForwardedFor": "信頼できる X-Forwarded-For",
"addressPortStrategy": "アドレス+ポート戦略",
"tryDelayMs": "試行遅延 (ms)",
"prioritizeIPv6": "IPv6 優先",
"interleave": "Interleave",
"maxConcurrentTry": "最大同時試行",
"customSockopt": "カスタム sockopt",
"addCustomOption": "カスタムオプション追加",
"serverNameIndication": "SNI",
"cipherSuites": "Cipher Suites",
"autoOption": "自動",
"minMaxVersion": "最小/最大バージョン",
"rejectUnknownSni": "未知の SNI を拒否",
"disableSystemRoot": "System Root を無効化",
"sessionResumption": "セッション再開",
"oneTimeLoading": "一度のみ読み込み",
"usageOption": "使用オプション",
"buildChain": "Build Chain",
"echKey": "ECH key",
"echConfig": "ECH config",
"getNewEchCert": "新しい ECH 証明書を取得",
"show": "表示",
"xver": "Xver",
"target": "ターゲット",
"maxTimeDiff": "最大時間差 (ms)",
"minClientVer": "最小クライアントバージョン",
"maxClientVer": "最大クライアントバージョン",
"shortIds": "Short IDs",
"spiderX": "SpiderX",
"getNewCert": "新しい証明書を取得",
"mldsa65Seed": "mldsa65 Seed",
"mldsa65Verify": "mldsa65 Verify",
"getNewSeed": "新しい Seed を取得"
},
"info": {
"mode": "モード",
"grpcServiceName": "grpc serviceName",
"grpcMultiMode": "grpc multiMode",
"interfaceName": "インターフェース名",
"mtu": "MTU",
"gateway": "Gateway",
"dns": "DNS",
"outboundsInterface": "アウトバウンドインターフェース",
"autoSystemRoutes": "自動システムルート",
"followRedirect": "FollowRedirect",
"auth": "Auth",
"noKernelTun": "非カーネル TUN",
"keepAlive": "Keep alive",
"peerNumber": "Peer {n}",
"peerNumberConfig": "Peer {n} 設定"
},
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"stream": {
"general": {
"request": "リクエスト",
"response": "レスポンス",
"name": "名前",
"value": "値"
},
"tcp": {
"version": "バージョン",
"method": "方法",
"path": "パス",
"status": "ステータス",
"statusDescription": "ステータス説明",
"requestHeader": "リクエストヘッダー",
"responseHeader": "レスポンスヘッダー"
}
}
},
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"clients": {
"add": "クライアントを追加",
"edit": "クライアントを編集",
"submitAdd": "クライアントを追加",
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
"submitEdit": "変更を保存",
"clientCount": "クライアント数",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"bulk": "一括追加",
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
"copyFromInbound": "インバウンドからクライアントをコピー",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"copyToInbound": "コピー先",
"copySelected": "選択をコピー",
"copySource": "コピー元",
"copyEmailPreview": "生成されるメールのプレビュー",
"copySelectSourceFirst": "まずコピー元のインバウンドを選択してください。",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"copyResult": "コピー結果",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"copyResultSuccess": "コピーに成功しました",
"copyResultNone": "コピーする対象がありません。クライアントが選択されていないか、コピー元が空です",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"copyResultErrors": "コピーエラー",
"copyFlowLabel": "新規クライアントの Flow (VLESS)",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"copyFlowHint": "コピーされる全クライアントに適用されます。空欄でスキップします。",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"selectAll": "すべて選択",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"clearAll": "すべてクリア",
"method": "メソッド",
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
"first": "最初",
"last": "最後",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"ipLog": "IP ログ",
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
"prefix": "プレフィックス",
"postfix": "サフィックス",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"delayedStart": "初回使用から開始",
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
"expireDays": "期間",
"days": "日",
"renew": "自動更新",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"renewDesc": "有効期限切れ後に自動更新します。(0 = 無効) (単位: 日)",
"searchPlaceholder": "メール、コメント、sub ID、UUID、パスワード、auth を検索…",
"filterTitle": "クライアントをフィルタ",
"clearAllFilters": "すべてクリア",
"sortOldest": "古い順",
"sortNewest": "新しい順",
"sortRecentlyUpdated": "最近更新",
"sortRecentlyOnline": "最近オンライン",
"sortEmailAZ": "メール A→Z",
"sortEmailZA": "メール Z→A",
"sortMostTraffic": "トラフィック多い順",
"sortHighestRemaining": "残量多い順",
"sortExpiringSoonest": "もうすぐ期限切れ",
"has": "あり",
"hasNot": "なし",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"title": "クライアント",
"actions": "操作",
"totalGB": "送受信合計 (GB)",
"expiryTime": "有効期限",
"addClients": "クライアントを追加",
"limitIp": "IP 制限",
"password": "パスワード",
"subId": "サブスクリプション ID",
"online": "オンライン",
"email": "メール",
"group": "グループ",
"groupDesc": "関連クライアントをまとめる論理ラベル(チーム、顧客、地域など)。ツールバーからフィルタ可能。",
"groupPlaceholder": "例: customer-a",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"comment": "コメント",
"traffic": "トラフィック",
"offline": "オフライン",
"addTitle": "クライアントを追加",
"qrCode": "QR コード",
"moreInformation": "詳細情報",
"delete": "削除",
"reset": "トラフィックをリセット",
"editTitle": "クライアントを編集",
"client": "クライアント",
"enabled": "有効",
"remaining": "残量",
"duration": "期間",
"attachedInbounds": "関連付けされたインバウンド",
"selectInbound": "1 つ以上のインバウンドを選択",
"noSubId": "このクライアントには subId がなく、共有可能なリンクはありません。",
"noLinks": "共有可能なリンクがありません — まずこのクライアントを対応するプロトコルのインバウンドに関連付けてください。",
"link": "リンク",
"resetNotPossible": "まずこのクライアントをインバウンドに関連付けてください。",
"general": "一般",
"resetAllTraffics": "すべてのクライアントのトラフィックをリセット",
"resetAllTrafficsTitle": "すべてのクライアントのトラフィックをリセットしますか?",
"resetAllTrafficsContent": "すべてのクライアントの送受信カウンターがゼロにリセットされます。クォータと有効期限には影響しません。元に戻せません。",
"deleteConfirmTitle": "クライアント {email} を削除しますか?",
"deleteConfirmContent": "クライアントを関連付けされたすべてのインバウンドから削除し、トラフィック記録も破棄します。元に戻せません。",
"deleteSelected": "削除 ({count})",
Bulk extend client expiry / traffic + clients page polish (#4499) * chore(sub): drop unused getFallbackMaster projectThroughFallbackMaster fully supersedes it for both panel-tracked and legacy unix-socket fallbacks. * feat(clients): bulk extend expiry / traffic for selected clients Adds POST /panel/api/clients/bulkAdjust which shifts ExpiryTime by addDays and TotalGB by addBytes for every email in one request. The endpoint is wired into the clients page through a new ClientBulkAdjustModal that opens from the existing multi-select toolbar. Clients with unlimited expiry (expiryTime=0) or unlimited traffic (totalGB=0) are skipped for the corresponding field so bulk extend never accidentally converts an unlimited client to a limited one. Negative values are allowed for refunds / corrections. Translations added for all 13 locales. * fix(db): silence GORM record-not-found spam in debug mode getSetting handles ErrRecordNotFound via database.IsNotFound and falls back to defaults, but GORM's Default logger still logs each miss as an error. With periodic jobs reading unset keys (xrayTemplateConfig, externalTrafficInformEnable) the panel log flooded thousands of times. Switch to a logger.New with IgnoreRecordNotFoundError=true so legitimate slow-query and SQL traces still surface in debug mode. * fix(clients): include inboundsById in columns memo deps Without it, the table's first paint captured an empty inboundsById and rendered each attached inbound as #<id>. Once a sort/filter forced the memo to rebuild it self-corrected, hence the visible flicker on reload. * fix(clients): handle delayed-start expiry in bulk adjust Negative ExpiryTime encodes a delay duration (magnitude = ms until the trial begins on first use). Adding positive addDays was simply arithmetically added, so e.g. a -7d delay + 30d turned into +23d since epoch (1970), making the client instantly expired. Branch on sign now: positive ExpiryTime extends additively, negative extends by subtracting so the value stays negative (more delay). Cross-sign reductions are skipped with an explicit reason instead of silently corrupting the field. * fix(clients): step traffic input by 1 GB instead of 0.1 The +/- buttons on the Total Sent/Received field nudged in 0.1 GB increments which is too granular for typical use. Set step=1 so each press moves a whole GB; users can still type decimal values directly. * fix(inbounds): step Total Flow input by 1 GB instead of 0.1 Matches the same nudge fix applied to the client form's Total Sent/Received field.
2026-05-23 14:27:20 +00:00
"adjustSelected": "調整 ({count})",
"subLinksSelected": "サブリンク ({count})",
"addToGroupTitle": "{count} クライアントをグループに追加",
"addToGroupTooltip": "既存のグループを選ぶか新しい名前を入力してください。Ungroup で現在のグループから外せます。",
"addToGroupPlaceholder": "グループ名",
"addToGroupSuccessToast": "{count} クライアントを {group} に追加しました",
"ungroupSuccessToast": "{count} クライアントのグループをクリアしました",
"ungroup": "グループ解除",
"ungroupConfirmTitle": "{count} クライアントをグループから外しますか?",
"ungroupConfirmContent": "選択したクライアントのグループラベルをクリアします。クライアント自体は保持されます (完全に削除するには Delete を使用)。",
"addToGroup": "グループに追加",
"attach": "アタッチ",
"adjust": "調整",
"subLinks": "サブリンク",
"selectedCount": "{count} 選択中",
"attachSelected": "アタッチ ({count})",
"attachToInboundsTitle": "{count} クライアントをインバウンドにアタッチ",
"attachToInboundsDesc": "選択した {count} クライアント(同じ UUID/パスワードと共有トラフィック)を選択したインバウンドにアタッチします。既存のアタッチは維持されます。",
"attachToInboundsTargets": "ターゲットインバウンド",
"attachToInboundsNoTargets": "アタッチ可能なマルチユーザーインバウンドがありません。",
"detachSelected": "デタッチ ({count})",
"detach": "デタッチ",
"detachFromInboundsTitle": "{count} クライアントをインバウンドからデタッチ",
"detachFromInboundsDesc": "選択した {count} クライアントを選択したインバウンドから外します。アタッチされていなかったペアは黙ってスキップされます。クライアントレコードは保持されます (完全に削除するには Delete を使用)。",
"detachFromInboundsTargets": "デタッチ対象のインバウンド",
"detachFromInboundsNoTargets": "マルチユーザーインバウンドがありません。",
"detachFromInboundsResult": "デタッチ {detached}、スキップ {skipped}。",
"detachFromInboundsResultMixed": "デタッチ {detached}、スキップ {skipped}、エラー {errors}。",
"subLinksTitle": "サブリンク ({count})",
"subLinkColumn": "サブスクリプション URL",
"subJsonLinkColumn": "サブスクリプション JSON URL",
"subLinksCopyAll": "すべてコピー",
"subLinksCopiedAll": "{count} リンクをコピーしました",
"subLinksEmpty": "選択したクライアントにはサブスクリプション ID がありません。",
"subLinksDisabled": "サブスクリプションサービスは無効です。",
"subLinksDisabledHint": "リンクを生成するにはパネル設定 → サブスクリプションで有効にしてください。",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"bulkDeleteConfirmTitle": "{count} 件のクライアントを削除しますか?",
"bulkDeleteConfirmContent": "選択された各クライアントを関連付けされたすべてのインバウンドから削除し、トラフィック記録も破棄します。元に戻せません。",
Bulk extend client expiry / traffic + clients page polish (#4499) * chore(sub): drop unused getFallbackMaster projectThroughFallbackMaster fully supersedes it for both panel-tracked and legacy unix-socket fallbacks. * feat(clients): bulk extend expiry / traffic for selected clients Adds POST /panel/api/clients/bulkAdjust which shifts ExpiryTime by addDays and TotalGB by addBytes for every email in one request. The endpoint is wired into the clients page through a new ClientBulkAdjustModal that opens from the existing multi-select toolbar. Clients with unlimited expiry (expiryTime=0) or unlimited traffic (totalGB=0) are skipped for the corresponding field so bulk extend never accidentally converts an unlimited client to a limited one. Negative values are allowed for refunds / corrections. Translations added for all 13 locales. * fix(db): silence GORM record-not-found spam in debug mode getSetting handles ErrRecordNotFound via database.IsNotFound and falls back to defaults, but GORM's Default logger still logs each miss as an error. With periodic jobs reading unset keys (xrayTemplateConfig, externalTrafficInformEnable) the panel log flooded thousands of times. Switch to a logger.New with IgnoreRecordNotFoundError=true so legitimate slow-query and SQL traces still surface in debug mode. * fix(clients): include inboundsById in columns memo deps Without it, the table's first paint captured an empty inboundsById and rendered each attached inbound as #<id>. Once a sort/filter forced the memo to rebuild it self-corrected, hence the visible flicker on reload. * fix(clients): handle delayed-start expiry in bulk adjust Negative ExpiryTime encodes a delay duration (magnitude = ms until the trial begins on first use). Adding positive addDays was simply arithmetically added, so e.g. a -7d delay + 30d turned into +23d since epoch (1970), making the client instantly expired. Branch on sign now: positive ExpiryTime extends additively, negative extends by subtracting so the value stays negative (more delay). Cross-sign reductions are skipped with an explicit reason instead of silently corrupting the field. * fix(clients): step traffic input by 1 GB instead of 0.1 The +/- buttons on the Total Sent/Received field nudged in 0.1 GB increments which is too granular for typical use. Set step=1 so each press moves a whole GB; users can still type decimal values directly. * fix(inbounds): step Total Flow input by 1 GB instead of 0.1 Matches the same nudge fix applied to the client form's Total Sent/Received field.
2026-05-23 14:27:20 +00:00
"bulkAdjustTitle": "{count} 件のクライアントを調整",
"bulkAdjustHint": "正の値は延長、負の値は短縮します。無期限の有効期限または無制限のトラフィックを持つクライアントは、その項目についてスキップされます。",
"bulkAdjustNothing": "適用する前に日数またはトラフィックを設定してください。",
"addDays": "日数を追加",
"addTrafficGB": "トラフィックを追加 (GB)",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"delDepleted": "使い切ったクライアントを削除",
"delDepletedConfirmTitle": "使い切ったクライアントを削除しますか?",
"delDepletedConfirmContent": "トラフィック上限に達したか有効期限が切れたクライアントをすべて削除します。元に戻せません。",
"auth": "Auth",
"hysteriaAuth": "Hysteria Auth",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"uuid": "UUID",
"flow": "Flow",
"vmessSecurity": "VMess セキュリティ",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"reverseTag": "Reverse tag",
"reverseTagPlaceholder": "任意の Reverse tag",
"telegramId": "Telegram ユーザー ID",
"telegramIdPlaceholder": "数値の Telegram ユーザー ID (0 = なし)",
"created": "作成日",
"updated": "更新日",
"ipLimit": "IP 制限",
"toasts": {
"deleted": "クライアントを削除しました",
"trafficReset": "トラフィックをリセットしました",
"allTrafficsReset": "すべてのクライアントのトラフィックをリセットしました",
"bulkDeleted": "{count} 件のクライアントを削除しました",
"bulkDeletedMixed": "{ok} 件削除、{failed} 件失敗",
"bulkCreated": "{count} 件のクライアントを作成しました",
"bulkCreatedMixed": "{ok} 件作成、{failed} 件失敗",
Bulk extend client expiry / traffic + clients page polish (#4499) * chore(sub): drop unused getFallbackMaster projectThroughFallbackMaster fully supersedes it for both panel-tracked and legacy unix-socket fallbacks. * feat(clients): bulk extend expiry / traffic for selected clients Adds POST /panel/api/clients/bulkAdjust which shifts ExpiryTime by addDays and TotalGB by addBytes for every email in one request. The endpoint is wired into the clients page through a new ClientBulkAdjustModal that opens from the existing multi-select toolbar. Clients with unlimited expiry (expiryTime=0) or unlimited traffic (totalGB=0) are skipped for the corresponding field so bulk extend never accidentally converts an unlimited client to a limited one. Negative values are allowed for refunds / corrections. Translations added for all 13 locales. * fix(db): silence GORM record-not-found spam in debug mode getSetting handles ErrRecordNotFound via database.IsNotFound and falls back to defaults, but GORM's Default logger still logs each miss as an error. With periodic jobs reading unset keys (xrayTemplateConfig, externalTrafficInformEnable) the panel log flooded thousands of times. Switch to a logger.New with IgnoreRecordNotFoundError=true so legitimate slow-query and SQL traces still surface in debug mode. * fix(clients): include inboundsById in columns memo deps Without it, the table's first paint captured an empty inboundsById and rendered each attached inbound as #<id>. Once a sort/filter forced the memo to rebuild it self-corrected, hence the visible flicker on reload. * fix(clients): handle delayed-start expiry in bulk adjust Negative ExpiryTime encodes a delay duration (magnitude = ms until the trial begins on first use). Adding positive addDays was simply arithmetically added, so e.g. a -7d delay + 30d turned into +23d since epoch (1970), making the client instantly expired. Branch on sign now: positive ExpiryTime extends additively, negative extends by subtracting so the value stays negative (more delay). Cross-sign reductions are skipped with an explicit reason instead of silently corrupting the field. * fix(clients): step traffic input by 1 GB instead of 0.1 The +/- buttons on the Total Sent/Received field nudged in 0.1 GB increments which is too granular for typical use. Set step=1 so each press moves a whole GB; users can still type decimal values directly. * fix(inbounds): step Total Flow input by 1 GB instead of 0.1 Matches the same nudge fix applied to the client form's Total Sent/Received field.
2026-05-23 14:27:20 +00:00
"bulkAdjusted": "{count} 件のクライアントを調整しました",
"bulkAdjustedMixed": "{ok} 件調整、{skipped} 件スキップ",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"delDepleted": "使い切った {count} 件のクライアントを削除しました"
}
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
},
"groups": {
"title": "グループ",
"name": "名前",
"clientCount": "グループ内のクライアント",
"totalGroups": "グループ合計",
"totalGroupedClients": "グループのあるクライアント",
"emptyGroups": "空のグループ",
"addGroup": "グループ追加",
"createSuccess": "グループ「{name}」を作成しました。",
"rename": "名前変更",
"renameTitle": "{name} の名前を変更",
"renameCollision": "「{name}」という名前のグループは既に存在します。",
"renameSuccess": "{count} クライアントのグループ名を変更しました。",
"deleteConfirmTitle": "グループ {name} を削除?",
"deleteConfirmContent": "これはグループを削除し、{count} クライアントのラベルをクリアします。クライアント自体は削除されません。",
"deleteSuccess": "{count} クライアントのグループをクリアしました。",
"resetTraffic": "トラフィックをリセット",
"resetConfirmTitle": "グループ {name} のトラフィックをリセット?",
"resetConfirmContent": "このグループ内のすべての {count} クライアントの up/down をゼロにします。",
"resetSuccess": "{count} クライアントのトラフィックをリセットしました。",
"adjustSuccess": "{name} 内の {count} クライアントを調整しました。",
"emptyForAction": "このグループにはまだクライアントがありません。",
"deleteGroupOnly": "グループ削除 (クライアントは保持)",
"deleteClients": "グループのクライアントを削除",
"deleteClientsConfirmTitle": "{name} 内のすべてのクライアントを削除?",
"deleteClientsConfirmContent": "これは {count} クライアントとそのトラフィック記録を永久に削除します。グループラベルもクリアされます。取り消せません。",
"deleteClientsSuccess": "{count} クライアントを削除しました。",
"deleteClientsMixed": "{ok} 削除、{failed} スキップ",
"addToGroup": "クライアントを追加…",
"addToGroupTitle": "グループ「{name}」にクライアントを追加",
"addToGroupDesc": "このグループに追加するクライアントを選択してください。既存のインバウンドアタッチは保持され、グループラベルのみ変更されます。すでにこのグループにいるクライアントは表示されません。",
"addToGroupEmpty": "追加可能な他のクライアントはありません。",
"addToGroupResult": "{count} クライアントを {name} に追加しました。",
"removeFromGroup": "クライアントを削除…",
"removeFromGroupTitle": "グループ「{name}」からクライアントを削除",
"removeFromGroupDesc": "このグループから外すメンバーを選択してください。クライアント自体は保持されます (完全に削除するには「グループのクライアントを削除」を使用)。",
"removeFromGroupResult": "{count} クライアントを {name} から外しました。"
},
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"nodes": {
"title": "ノード",
"addNode": "ノードを追加",
"editNode": "ノード編集",
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
"totalNodes": "ノード総数",
"onlineNodes": "オンライン",
"offlineNodes": "オフライン",
"avgLatency": "平均レイテンシ",
"name": "名前",
"namePlaceholder": "例: de-frankfurt-1",
"addressPlaceholder": "panel.example.com または 1.2.3.4",
"remark": "備考",
"scheme": "スキーム",
"address": "アドレス",
"port": "ポート",
"basePath": "ベースパス",
"apiToken": "API トークン",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"apiTokenPlaceholder": "リモートパネルの設定ページから取得したトークン",
"apiTokenHint": "リモートパネルでは、設定 → APIトークン でAPIトークンを確認できます。",
"regenerate": "トークンを再生成",
"regenerateConfirm": "再生成すると現在のトークンは無効になります。これを使用しているすべての中央パネルは更新されるまでアクセスできなくなります。続行しますか?",
"allowPrivateAddress": "プライベートアドレスを許可",
"allowPrivateAddressHint": "プライベートネットワークまたはVPN上のードにのみ有効にします。",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"enable": "有効",
"status": "ステータス",
"cpu": "CPU",
"mem": "メモリ",
"uptime": "稼働時間",
"latency": "レイテンシ",
"lastHeartbeat": "最後のハートビート",
"xrayVersion": "Xrayバージョン",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"panelVersion": "パネルのバージョン",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"actions": "操作",
"probe": "今すぐプローブ",
"testConnection": "接続テスト",
"connectionOk": "接続OK ({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": "パネルを再起動",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"restartPanelDesc": "パネルを再起動してもよろしいですか?再起動後にパネルにアクセスできない場合は、サーバーでパネルログを確認してください",
"restartPanelSuccess": "パネルの再起動に成功しました",
"actions": "操作",
"resetDefaultConfig": "デフォルト設定にリセット",
"panelSettings": "一般",
"securitySettings": "セキュリティ設定",
"securityWarnings": "セキュリティ警告",
"panelExposed": "パネルが露出している可能性があります:",
"warnHttp": "パネルが平文 HTTP で提供されています — 本番環境には TLS を設定してください。",
"warnDefaultPort": "デフォルトポート 2053 はよく知られています — ランダムなポートに変更してください。",
"warnDefaultBasePath": "デフォルトのベースパス \"/\" はよく知られています — ランダムなパスに変更してください。",
"warnDefaultSubPath": "デフォルトのサブスクリプションパス \"/sub/\" はよく知られています — 変更してください。",
"warnDefaultJsonPath": "デフォルトの JSON サブスクリプションパス \"/json/\" はよく知られています — 変更してください。",
"TGBotSettings": "Telegram Bot",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"panelListeningIP": "パネル監視IP",
"panelListeningIPDesc": "デフォルトではすべてのIPを監視する",
"panelListeningDomain": "パネル監視ドメイン",
"panelListeningDomainDesc": "デフォルトで空白の場合、すべてのドメインとIPアドレスを監視する",
"panelPort": "パネル監視ポート",
"panelPortDesc": "再起動で有効",
"publicKeyPath": "パネル証明書公開鍵ファイルパス",
"publicKeyPathDesc": "'/'で始まる絶対パスを入力",
"privateKeyPath": "パネル証明書秘密鍵ファイルパス",
"privateKeyPathDesc": "'/'で始まる絶対パスを入力",
"panelUrlPath": "URI パス",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"panelUrlPathDesc": "'/'で始まり、'/'で終わる必要があります",
"pageSize": "ページサイズ",
"pageSizeDesc": "インバウンドテーブルのページサイズを定義します。0を設定すると無効化されます",
"panelProxy": "パネルネットワークプロキシ",
"panelProxyDesc": "パネル自体のアウトバウンドリクエスト (geo 更新、Xray/パネルバージョンチェック、Telegram) をこのプロキシ経由でルーティングし、サーバー側の GitHub/Telegram フィルタリングを回避します。socks5:// または http(s):// を受け付けます。例: ローカルの Xray SOCKS インバウンド。直接接続するには空のままにします。",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"remarkModel": "備考モデルと区切り記号",
"datepicker": "日付ピッカー",
"datepickerPlaceholder": "日付を選択",
"datepickerDescription": "日付選択カレンダーで有効期限を指定する",
"sampleRemark": "備考の例",
"oldUsername": "旧ユーザー名",
"currentPassword": "旧パスワード",
"newUsername": "新しいユーザー名",
"newPassword": "新しいパスワード",
"telegramBotEnable": "Telegramボットを有効にする",
"telegramBotEnableDesc": "Telegramボット機能を有効にする",
"telegramToken": "Telegram トークン",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"telegramTokenDesc": "'{'@'}BotFather'から取得したTelegramボットトークン",
"telegramProxy": "SOCKS プロキシ",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"telegramProxyDesc": "SOCKS5プロキシを有効にしてTelegramに接続するガイドに従って設定を調整",
"telegramAPIServer": "Telegram API サーバー",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"telegramAPIServerDesc": "使用するTelegram APIサーバー。空白の場合はデフォルトサーバーを使用する",
"telegramChatId": "管理者チャットID",
"telegramChatIdDesc": "Telegram管理者チャットID複数の場合はカンマで区切る{'@'}userinfobotで取得するか、ボットで'/id'コマンドを使用して取得する",
"telegramNotifyTime": "通知時間",
"telegramNotifyTimeDesc": "定期的なTelegramボット通知時間を設定するcrontab時間形式を使用",
"tgNotifyBackup": "データベースバックアップ",
"tgNotifyBackupDesc": "レポート付きのデータベースバックアップファイルを送信",
"tgNotifyLogin": "ログイン通知",
"tgNotifyLoginDesc": "誰かがパネルにログインしようとしたときに、ユーザー名、IPアドレス、時間を表示する",
"sessionMaxAge": "セッション期間",
"sessionMaxAgeDesc": "ログイン状態を保持する期間(単位:分)",
"expireTimeDiff": "有効期限通知のしきい値",
"expireTimeDiffDesc": "このしきい値に達した場合、有効期限に関する通知を受け取る(単位:日)",
"trafficDiff": "トラフィック消耗しきい値",
"trafficDiffDesc": "このしきい値に達した場合、トラフィック消耗に関する通知を受け取る単位GB",
"tgNotifyCpu": "CPU負荷通知しきい値",
"tgNotifyCpuDesc": "CPU負荷がこのしきい値を超えた場合、通知を受け取る単位%",
"timeZone": "タイムゾーン",
"timeZoneDesc": "定時タスクはこのタイムゾーンの時間に従って実行される",
"subSettings": "サブスクリプション設定",
"subEnable": "サブスクリプションサービスを有効にする",
"subEnableDesc": "サブスクリプションサービス機能を有効にする",
"subJsonEnable": "JSON サブスクリプションのエンドポイントを個別に有効/無効にする。",
"subJsonEnableTitle": "JSON サブスクリプション",
"subClashEnableTitle": "Clash / Mihomo サブスクリプション",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"subTitle": "サブスクリプションタイトル",
"subTitleDesc": "VPNクライアントに表示されるタイトル",
"subSupportUrl": "サポートURL",
"subSupportUrlDesc": "VPNクライアントに表示されるテクニカルサポートへのリンク",
"subProfileUrl": "プロフィールURL",
"subProfileUrlDesc": "VPNクライアントに表示されるWebサイトへのリンク",
"subAnnounce": "お知らせ",
"subAnnounceDesc": "VPNクライアントに表示されるお知らせのテキスト",
"subEnableRouting": "ルーティングを有効化",
"subEnableRoutingDesc": "VPNクライアントでルーティングを有効にするためのグローバル設定。(Happのみ)",
"subRoutingRules": "ルーティングルール",
"subRoutingRulesDesc": "VPNクライアントのグローバルルーティングルール。(Happのみ)",
"subListen": "監視IP",
"subListenDesc": "サブスクリプションサービスが監視するIPアドレス空白にするとすべてのIPを監視",
"subPort": "監視ポート",
"subPortDesc": "サブスクリプションサービスが監視するポート番号(使用されていないポートである必要があります)",
"subCertPath": "公開鍵パス",
"subCertPathDesc": "サブスクリプションサービスで使用する公開鍵ファイルのパス('/'で始まる)",
"subKeyPath": "秘密鍵パス",
"subKeyPathDesc": "サブスクリプションサービスで使用する秘密鍵ファイルのパス('/'で始まる)",
"subPath": "URI パス",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"subPathDesc": "サブスクリプションサービスで使用するURIパス'/'で始まり、'/'で終わる)",
"subDomain": "監視ドメイン",
"subDomainDesc": "サブスクリプションサービスが監視するドメイン空白にするとすべてのドメインとIPを監視",
"subUpdates": "更新間隔",
"subUpdatesDesc": "クライアントアプリケーションでサブスクリプションURLの更新間隔単位時間",
"subEncrypt": "エンコード",
"subEncryptDesc": "サブスクリプションサービスが返す内容をBase64エンコードする",
"subShowInfo": "利用情報を表示",
"subShowInfoDesc": "クライアントアプリで残りのトラフィックと日付情報を表示する",
"subEmailInRemark": "名前にメールを含める",
"subEmailInRemarkDesc": "サブスクリプションプロファイル名にクライアントのメールアドレスを含めます。",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"subURI": "リバースプロキシURI",
"subURIDesc": "プロキシ後ろのサブスクリプションURLのURIパスに使用する",
"externalTrafficInformEnable": "外部トラフィック情報",
"externalTrafficInformEnableDesc": "トラフィック更新ごとに外部 API に通知。",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"externalTrafficInformURI": "外部トラフィック通知 URI",
"externalTrafficInformURIDesc": "トラフィックの更新ごとに外部 API に通知します。",
"restartXrayOnClientDisable": "自動無効化後に Xray を再起動",
"restartXrayOnClientDisableDesc": "有効期限切れまたはトラフィック上限でクライアントが自動的に無効化されたとき、Xray を再起動します。",
"fragment": "フラグメント",
"fragmentDesc": "TLS helloパケットのフラグメントを有効にする",
"fragmentSett": "設定",
"noisesDesc": "Noisesを有効にする",
"noisesSett": "Noises設定",
"trustedProxyCidrs": "信頼できるプロキシ CIDR",
"trustedProxyCidrsDesc": "転送される host、proto、クライアント IP ヘッダーを設定可能な IP/CIDR (カンマ区切り)。",
"ldap": {
"enable": "LDAP 同期を有効化",
"host": "LDAP host",
"port": "LDAP ポート",
"useTls": "TLS (LDAPS) を使用",
"bindDn": "Bind DN",
"passwordConfigured": "設定済み;現在のパスワードを保持するには空のままにします。",
"passwordUnconfigured": "未設定。",
"passwordPlaceholder": "設定済み — 置き換えるには新しい値を入力",
"baseDn": "Base DN",
"userFilter": "ユーザーフィルター",
"userAttr": "ユーザー属性 (username/email)",
"vlessField": "VLESS flag 属性",
"flagField": "汎用 flag 属性 (任意)",
"flagFieldDesc": "設定すると VLESS flag を上書きします — 例: shadowInactive。",
"truthyValues": "Truthy 値",
"truthyValuesDesc": "カンマ区切り;デフォルト: true,1,yes,on",
"invertFlag": "flag を反転",
"invertFlagDesc": "属性が「無効」を意味する場合に有効化 (例: shadowInactive)。",
"syncSchedule": "同期スケジュール",
"syncScheduleDesc": "cron 風の文字列、例 @every 1m",
"inboundTags": "インバウンドタグ",
"inboundTagsDesc": "LDAP 同期がクライアントを自動作成/削除できるインバウンド。",
"noInbounds": "インバウンドが見つかりません。先にインバウンドで作成してください。",
"autoCreate": "クライアントを自動作成",
"autoDelete": "クライアントを自動削除",
"defaultTotalGb": "デフォルト合計 (GB)",
"defaultExpiryDays": "デフォルト有効期限 (日)",
"defaultIpLimit": "デフォルト IP 制限"
},
"subFormats": {
"packets": "パケット",
"length": "長さ",
"interval": "間隔",
"maxSplit": "最大分割",
"noises": "ノイズ",
"noiseItem": "ノイズ №{n}",
"type": "種類",
"packet": "パケット",
"delayMs": "遅延 (ms)",
"applyTo": "適用先",
"addNoise": "+ ノイズ",
"concurrency": "並行数",
"xudpConcurrency": "xudp 並行数",
"xudpUdp443": "xudp UDP 443"
},
"mux": "Mux",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"muxDesc": "確立されたストリーム内で複数の独立したストリームを伝送する",
"muxSett": "マルチプレクサ設定",
"direct": "直接接続",
"directDesc": "特定の国のドメインまたはIP範囲に直接接続する",
"notifications": "通知",
"certs": "証明書",
"externalTraffic": "外部トラフィック",
"dateAndTime": "日付と時刻",
"proxyAndServer": "プロキシとサーバー",
"intervals": "間隔",
"information": "情報",
"language": "言語",
"telegramBotLanguage": "Telegram Botの言語",
"security": {
"admin": "管理者の資格情報",
"twoFactor": "二段階認証",
"twoFactorEnable": "2FAを有効化",
"twoFactorEnableDesc": "セキュリティを強化するために追加の認証層を追加します。",
"twoFactorModalSetTitle": "二段階認証を有効にする",
"twoFactorModalDeleteTitle": "二段階認証を無効にする",
"twoFactorModalSteps": "二段階認証を設定するには、次の手順を実行してください:",
"twoFactorModalFirstStep": "1. 認証アプリでこのQRコードをスキャンするか、QRコード近くのトークンをコピーしてアプリに貼り付けます",
"twoFactorModalSecondStep": "2. アプリからコードを入力してください",
"twoFactorModalRemoveStep": "二段階認証を削除するには、アプリからコードを入力してください。",
"twoFactorModalChangeCredentialsTitle": "認証情報の変更",
"twoFactorModalChangeCredentialsStep": "管理者の認証情報を変更するには、アプリケーションからコードを入力してください。",
"twoFactorModalSetSuccess": "二要素認証が正常に設定されました",
"twoFactorModalDeleteSuccess": "二要素認証が正常に削除されました",
"twoFactorModalError": "コードが間違っています",
"show": "表示",
"hide": "非表示",
"apiTokenNew": "新規トークン",
"apiTokenName": "名前",
"apiTokenNamePlaceholder": "例: central-panel-a",
"apiTokenNameRequired": "名前は必須です",
"apiTokenEmpty": "トークンがまだありません — ボットやリモートパネルを認証するために作成してください。",
"apiTokenDeleteWarning": "このトークンを使用しているクライアントは直ちに認証できなくなります。"
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
},
"toasts": {
"modifySettings": "パラメーターが変更されました。",
"getSettings": "パラメーターの取得中にエラーが発生しました",
"modifyUserError": "管理者認証情報の変更中にエラーが発生しました。",
"modifyUser": "管理者の認証情報を正常に変更しました。",
"originalUserPassIncorrect": "旧ユーザー名または旧パスワードが間違っています",
"userPassMustBeNotEmpty": "新しいユーザー名と新しいパスワードは空にできません",
"getOutboundTrafficError": "送信トラフィックの取得エラー",
"resetOutboundTrafficError": "送信トラフィックのリセットエラー"
}
},
"xray": {
"title": "Xray 設定",
"save": "保存",
"restart": "Xray を再起動",
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
"restartSuccess": "Xrayの再起動に成功しました",
"restartOutputTitle": "Xray 再起動の出力",
"restartConfirmTitle": "xray を再起動?",
"restartConfirmContent": "保存された構成で xray サービスを再ロードします。",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"stopSuccess": "Xrayが正常に停止しました",
"restartError": "Xrayの再起動中にエラーが発生しました。",
"stopError": "Xrayの停止中にエラーが発生しました。",
"basicTemplate": "基本設定",
"advancedTemplate": "高度な設定",
"generalConfigs": "一般設定",
"generalConfigsDesc": "これらのオプションは一般設定を決定します",
"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": "インバウンド",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"InboundsDesc": "特定のクライアントからのトラフィックを受け入れる",
"Outbounds": "アウトバウンド",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"Balancers": "負荷分散",
"balancerTagRequired": "タグは必須です",
"balancerSelectorRequired": "アウトバウンドを少なくとも1つ選んでください",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"OutboundsDesc": "アウトバウンドトラフィックの送信方法を設定する",
"Routings": "ルーティングルール",
"RoutingsDesc": "各ルールの優先順位が重要です",
"completeTemplate": "すべて",
"logLevel": "ログレベル",
"logLevelDesc": "エラーログのレベルを指定し、記録する情報を示します",
"accessLog": "アクセスログ",
"accessLogDesc": "アクセスログのファイルパス。特殊値 'none' はアクセスログを無効にします",
"errorLog": "エラーログ",
"errorLogDesc": "エラーログのファイルパス。特殊値 'none' はエラーログを無効にします",
"dnsLog": "DNS ログ",
"dnsLogDesc": "DNSクエリのログを有効にするかどうか",
"maskAddress": "アドレスをマスク",
"maskAddressDesc": "IPアドレスをマスクし、有効にするとログに表示されるIPアドレスを自動的に置き換えます",
"statistics": "統計",
"statsInboundUplink": "インバウンドアップロード統計",
"statsInboundUplinkDesc": "すべてのインバウンドプロキシのアップストリームトラフィックの統計収集を有効にします。",
"statsInboundDownlink": "インバウンドダウンロード統計",
"statsInboundDownlinkDesc": "すべてのインバウンドプロキシのダウンストリームトラフィックの統計収集を有効にします。",
"statsOutboundUplink": "アウトバウンドアップロード統計",
"statsOutboundUplinkDesc": "すべてのアウトバウンドプロキシのアップストリームトラフィックの統計収集を有効にします。",
"statsOutboundDownlink": "アウトバウンドダウンロード統計",
"statsOutboundDownlinkDesc": "すべてのアウトバウンドプロキシのダウンストリームトラフィックの統計収集を有効にします。",
"rules": {
"first": "最初",
"last": "最後",
"up": "上へ",
"down": "下へ",
"source": "ソース",
"dest": "宛先アドレス",
"inbound": "インバウンド",
"outbound": "アウトバウンド",
"balancer": "負荷分散",
"info": "情報",
"add": "ルール追加",
"edit": "ルール編集",
"useComma": "カンマ区切りの項目"
},
"routing": {
"dragToReorder": "ドラッグして並べ替え"
},
"ruleForm": {
"sourceIps": "送信元 IP",
"sourcePort": "送信元ポート",
"vlessRoute": "VLESS ルート",
"attributes": "属性",
"value": "値",
"user": "ユーザー",
"inboundTags": "インバウンドタグ",
"outboundTag": "アウトバウンドタグ",
"balancerTag": "バランサータグ",
"balancerTagTooltip": "設定済みのロードバランサーの1つを通じてトラフィックをルーティング"
},
"outboundForm": {
"tagDuplicate": "このタグは他のアウトバウンドで使用されています",
"tagRequired": "タグは必須です",
"tagPlaceholder": "一意のタグ",
"localIpPlaceholder": "ローカル IP",
"addressRequired": "アドレスは必須です",
"portRequired": "ポートは必須です",
"optional": "任意",
"udpOverTcp": "UDP over TCP",
"uotVersion": "UoT バージョン",
"inboundTag": "インバウンドタグ",
"inboundTagPlaceholder": "ルーティングルールで使うインバウンドタグ",
"responseType": "レスポンスタイプ",
"rewriteNetwork": "ネットワーク書き換え",
"unchanged": "(変更なし)",
"unchangedAddress": "(変更なし) 例: 1.1.1.1",
"rules": "ルール",
"ruleN": "ルール {n}",
"action": "アクション",
"redirect": "Redirect",
"fragment": "Fragment",
"finalRules": "最終ルール",
"overrideXrayPrivateIp": "Xray のデフォルトプライベート IP ブロックを上書き",
"blockDelay": "ブロック遅延 (ms)",
"reverseSniffing": "逆 sniffing",
"workers": "Workers",
"reserved": "予約",
"minUploadInterval": "最小アップロード間隔 (ms)",
"maxUploadSizeBytes": "最大アップロードサイズ (バイト)",
"uplinkChunkSize": "Uplink チャンクサイズ",
"noGrpcHeader": "gRPC ヘッダーなし",
"maxConcurrency": "最大同時実行数",
"maxConnections": "最大接続数",
"maxReuseTimes": "最大再利用回数",
"maxRequestTimes": "最大リクエスト回数",
"maxReusableSecs": "最大再利用秒数",
"keepAlivePeriod": "keep alive 周期",
"authPassword": "Auth パスワード",
"visionTestpre": "Vision testpre",
"serverNamePlaceholder": "サーバー名",
"verifyPeerName": "peer 名を検証",
"pinnedSha256": "Pinned SHA256",
"shortId": "Short ID",
"sockopts": "Sockopts",
"keepAliveInterval": "keep alive 間隔",
"markFwmark": "Mark (fwmark)",
"interface": "インターフェース",
"ipv6Only": "IPv6 のみ",
"acceptProxyProtocol": "proxy protocol を受け入れる",
"tcpUserTimeoutMs": "TCP user timeout (ms)",
"tcpKeepAliveIdleS": "TCP keep-alive idle (秒)"
},
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"outbound": {
"addOutbound": "アウトバウンド追加",
"addReverse": "リバース追加",
"editOutbound": "アウトバウンド編集",
"editReverse": "リバース編集",
"reverseTag": "リバースタグ",
"reverseTagDesc": "VLESSシンプルリバースプロキシのアウトバウンドタグ。無効にするには空欄にしてください。",
"reverseTagPlaceholder": "アウトバウンドタグ(空欄で無効)",
"tag": "タグ",
"tagDesc": "一意のタグ",
"address": "アドレス",
"reverse": "リバース",
"domain": "ドメイン",
"type": "タイプ",
"bridge": "Bridge",
"portal": "Portal",
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
"link": "リンク",
"intercon": "インターコネクション",
"settings": "設定",
"accountInfo": "アカウント情報",
"outboundStatus": "アウトバウンドステータス",
"sendThrough": "送信経路",
"test": "テスト",
"testResult": "テスト結果",
"testing": "接続をテスト中...",
"testSuccess": "テスト成功",
"testFailed": "テスト失敗",
"testError": "アウトバウンドのテストに失敗しました",
"testModeTooltip": "TCP: 高速 dial-only プローブ。HTTP: xray を経由した完全リクエスト。",
"testAll": "すべてテスト",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"nordvpn": "NordVPN",
"accessToken": "アクセストークン",
"country": "国",
"server": "サーバー",
"city": "都市",
"allCities": "すべての都市",
"privateKey": "秘密鍵",
"load": "負荷"
},
"balancer": {
"addBalancer": "負荷分散追加",
"editBalancer": "負荷分散編集",
"balancerStrategy": "戦略",
"balancerSelectors": "セレクター",
"tag": "タグ",
"tagDesc": "一意のタグ",
"tagDuplicate": "このタグは他のバランサーで使用されています",
"tagPlaceholder": "一意のバランサータグ",
"selector": "セレクター",
"fallback": "Fallback",
"expected": "期待値",
"expectedPlaceholder": "最適ノード数",
"maxRtt": "最大 RTT",
"tolerance": "許容範囲",
"baselines": "Baselines",
"costs": "Costs",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"balancerDesc": "balancerTagとoutboundTagは同時に使用できません。同時に使用された場合、outboundTagのみが有効になります。"
},
"wireguard": {
"secretKey": "シークレットキー",
"publicKey": "公開鍵",
"allowedIPs": "許可されたIP",
"endpoint": "エンドポイント",
"psk": "共有キー",
"domainStrategy": "ドメイン戦略"
},
"tun": {
"nameDesc": "TUN インターフェースの名前。デフォルトは 'xray0' です",
"mtuDesc": "最大伝送単位。データパケットの最大サイズ。デフォルトは 1500 です",
"userLevel": "ユーザーレベル",
"userLevelDesc": "このインバウンドを通じて確立されたすべての接続は、このユーザーレベルを使用します。デフォルトは 0 です"
},
"nord": {
"accessToken": "Access token",
"privateKey": "秘密鍵",
"noServers": "選択した国のサーバーが見つかりません",
"noPublicKey": "選択したサーバーは NordLynx 公開鍵を公開していません。",
"outboundAdded": "NordVPN アウトバウンドを追加しました",
"outboundUpdated": "NordVPN アウトバウンドを更新しました"
},
"warp": {
"licenseError": "WARP ライセンスの設定に失敗しました。",
"fetchFirst": "先に WARP 構成を取得してください。",
"createAccount": "WARP アカウントを作成",
"accessToken": "Access token",
"deviceId": "デバイス ID",
"licenseKey": "ライセンスキー",
"privateKey": "秘密鍵",
"deleteAccount": "アカウントを削除",
"settings": "設定",
"licenseKeyLabel": "WARP / WARP+ ライセンスキー",
"key": "キー",
"keyPlaceholder": "26文字の WARP+ キー",
"accountInfo": "アカウント情報",
"deviceName": "デバイス名",
"deviceModel": "デバイスモデル",
"deviceEnabled": "デバイス有効",
"accountType": "アカウントタイプ",
"role": "役割",
"warpPlusData": "WARP+ データ",
"quota": "クォータ",
"usage": "使用量",
"addOutbound": "アウトバウンドを追加"
},
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"dns": {
"enable": "DNSを有効にする",
"enableDesc": "組み込みDNSサーバーを有効にする",
"tag": "DNSインバウンドタグ",
"tagDesc": "このタグはルーティングルールでインバウンドタグとして使用できます",
"clientIp": "クライアントIP",
"clientIpDesc": "DNSクエリ中に指定されたIPの位置をサーバーに通知するために使用されます",
"disableCache": "キャッシュを無効にする",
"disableCacheDesc": "DNSキャッシュを無効にします",
"disableFallback": "フォールバックを無効にする",
"disableFallbackDesc": "フォールバックDNSクエリを無効にします",
"disableFallbackIfMatch": "一致した場合にフォールバックを無効にする",
"disableFallbackIfMatchDesc": "DNSサーバーの一致するドメインリストにヒットした場合、フォールバックDNSクエリを無効にします",
"enableParallelQuery": "並列クエリを有効にする",
"enableParallelQueryDesc": "複数のサーバーへの並列DNSクエリを有効にして、より高速な解決を実現",
"strategy": "クエリ戦略",
"strategyDesc": "ドメイン名解決の全体的な戦略",
"add": "サーバー追加",
"edit": "サーバー編集",
"domains": "ドメイン",
"expectIPs": "期待されるIP",
"unexpectIPs": "予期しないIP",
"useSystemHosts": "システムのHostsを使用",
"useSystemHostsDesc": "インストール済みシステムのhostsファイルを使用する",
"serveStale": "期限切れキャッシュを使用",
"serveStaleDesc": "バックグラウンドで更新中に期限切れキャッシュ結果を返す",
"serveExpiredTTL": "期限切れTTL",
"serveExpiredTTLDesc": "期限切れキャッシュエントリの有効期間。0 = 無期限",
"timeoutMs": "タイムアウト (ms)",
"skipFallback": "フォールバックをスキップ",
"finalQuery": "最終クエリ",
"hosts": "Hosts",
"hostsAdd": "Host を追加",
"hostsEmpty": "Host が定義されていません",
"hostsDomain": "ドメイン (例: domain:example.com)",
"hostsValues": "IP またはドメイン — 入力して Enter",
Feat/multi inbound clients (#4469) * feat(clients): add shadow tables for first-class client promotion Introduces three new GORM-backed tables (clients, client_inbounds, inbound_fallback_children) and a populate-only seeder that backfills them from each inbound's existing settings.clients JSON. Duplicate emails across inbounds auto-merge under one client row, with each field conflict logged. Existing services are unchanged and continue reading from settings.clients — this commit is groundwork only. * feat(clients): make clients+client_inbounds the runtime source of truth Adds ClientService.SyncInbound that reconciles the new tables from each inbound's clients list whenever existing service paths mutate settings.clients. Wires it into AddInbound, UpdateInbound, AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, DelDepletedClients, autoRenewClients, and the timestamp-backfill path in adjustTraffics, plus DetachInbound on DelInbound. GetXrayConfig now builds settings.clients from the new tables before writing config.json, and getInboundsBySubId joins through them instead of JSON_EACH on settings JSON. Live Xray config and subscription endpoints are now driven by the relational view; settings.clients JSON stays in step as a side effect of every write. * feat(clients): add top-level Clients tab and CRUD API Adds /panel/api/clients endpoints (list, get, add, update, del, attach, detach) backed by ClientService methods that orchestrate the per-inbound Add/Update/Del flows so a single client row is created once and attached to many inbounds in one operation. The frontend gains a dedicated Clients page (frontend/clients.html + src/pages/clients/) with an AntD table, multi-inbound attach modal, and full CRUD. Axios interceptor learns to honour Content-Type: application/json so the JSON endpoints work alongside the legacy form-encoded ones. The legacy per-inbound client modal stays untouched in this PR — both flows now write to the same source of truth. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): add Port-with-Fallback inbound type Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound under the hood but is paired with a sidecar table of child inbounds. Panel auto-builds settings.fallbacks at Xray-config-gen time from the sidecar — each child's listen+port becomes the fallback dest, with SNI/ALPN/path/xver match criteria pulled from the row. No more typing loopback ports by hand or keeping settings.fallbacks in sync. Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON); two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren); xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the inbound model emits protocol="vless" so Xray accepts the config. Frontend: PORTFALLBACK joins the protocol dropdown; selecting it shows the standard VLESS controls plus a Fallback Children table (inbound picker + per-row SNI/ALPN/path/xver). Children are loaded on edit and replaced atomically on save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reset Traffic, QR Code, Info actions + Online/Remaining columns The Clients page table gains: - Online column — green/grey tag driven by /panel/api/inbounds/onlines, polled every 10s. - Remaining column — bytes-remaining tag, coloured green/orange/red against quota, purple infinity when unlimited. - Action icons per row: QR, Info, Reset traffic, Edit, Delete. ClientInfoModal shows the full client detail (uuid/password/auth, traffic ↑/↓ + remaining + all-time, expiry absolute + relative, attached inbounds chip list, online + last-online). ClientQrModal fetches links for the client's subId via /panel/api/inbounds/getSubLinks/:subId and renders each one through the existing QrPanel component. Reset Traffic confirms then calls the existing per-inbound endpoint on the client's first attached inbound (the traffic row is keyed on email globally, so any attached inbound resets the shared counter). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): expose Attached inbounds in edit mode The multi-select was gated on add-only, so editing a client had no way to change which inbounds it belonged to. The picker now shows in both modes, and on submit the modal diffs the picked set against the original attachedIds — additions go through the /attach endpoint, removals through /detach, both after the field update lands so the new attachments get the latest values. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): unbreak template parsing + stale i18n keys - InboundFormModal: split the multi-line help string in the PortFallback section onto one line — Vue's template parser was bailing on Unterminated string constant because a single-quoted literal spanned two lines inside a {{ }} interpolation. - ClientInfoModal: t('disable') was missing at the root level, so vue-i18n returned the key path literally. Use t('disabled') which exists. - Linter cleanup elsewhere: pages.client.* references renamed to pages.clients.* to match the merged i18n block; whitespace normalisation in a few unrelated Vue templates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * 1 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(traffic): drop all-time traffic tracking Removes the AllTime field from Inbound and ClientTraffic and migrates existing DBs by dropping the all_time columns on startup. The counter duplicated up+down without adding signal, and the per-event accumulator ran on every traffic write. Frontend: drop the All-time column from the inbound list and the client-row table, the All-time row from the client info modal, and the All-Time Total Usage tile from the inbounds summary card. The allTimeTraffic/allTimeTrafficUsage i18n keys are removed across every locale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): mobile cards, multi-select, bulk add Adds the same row-card layout the inbounds page uses on mobile: the table is suppressed under the mobile breakpoint and each client renders as a compact card with a status dot, email, Info button, Enable switch, and overflow menu. All the per-client detail (traffic, remaining, expiry, attached inbounds, flow, created/updated, URL, subscription) opens through the existing info modal. Multi-select with bulk delete wires AntD row-selection on desktop and a per-card checkbox on mobile; a Delete (N) button appears in the toolbar when anything is selected. Bulk add reuses the five email-generation modes from the inbound bulk modal but takes a multi-inbound picker so one bulk run can attach to several inbounds at once. Submits client-by-client through the existing /panel/api/clients/add endpoint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): remove legacy per-inbound client UI Now that clients live as first-class rows attached to one or many inbounds, the per-inbound client UI on the inbounds page is dead weight — every client action either has a global equivalent on the Clients page or makes no sense in a many-to-many world. Deletes ClientFormModal, ClientBulkModal, CopyClientsModal, and ClientRowTable from inbounds/. Strips the matching emits, refs, handlers, and dropdown menu items from InboundList and InboundsPage, and removes the dead mobile expand-chevron state and the desktop expanded-row plumbing that drove the inline client table. The InboundFormModal Clients tab still works in add-mode (one inline client at inbound creation) — that flow goes through ClientService. SyncInbound on save and remains useful. Fixes a stray "</a-dropdown>" left over by an earlier toolbar edit in ClientsPage that broke the template parser. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Delete depleted action Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): move every client-shaped endpoint off /inbounds onto /clients After the multi-inbound client migration, client state belongs to the client API surface, not the inbound one. Twelve routes that were crammed under /panel/api/inbounds/* now live where they belong, under /panel/api/clients/*. Moved (route, handler, doc): POST /clientIps/:email POST /clearClientIps/:email POST /onlines POST /lastOnline POST /updateClientTraffic/:email POST /resetAllClientTraffics/:id POST /delDepletedClients/:id POST /:id/resetClientTraffic/:email GET /getClientTraffics/:email GET /getClientTrafficsById/:id GET /getSubLinks/:subId GET /getClientLinks/:id/:email Their /clients/* counterparts are: POST /clients/clientIps/:email POST /clients/clearClientIps/:email POST /clients/onlines POST /clients/lastOnline POST /clients/updateTraffic/:email POST /clients/resetTraffic/:email (email-only, fans out) GET /clients/traffic/:email GET /clients/traffic/byId/:id GET /clients/subLinks/:subId GET /clients/links/:id/:email per-inbound resetAllClientTraffics and delDepletedClients are dropped entirely — the Clients page already exposes global Reset All Traffic and Delete depleted actions, and per-inbound resets are meaningless once a client can be attached to many inbounds. ClientService.ResetTrafficByEmail is the new email-only reset path: it looks up every inbound the client is attached to and pushes the counter reset + Xray re-add through inboundService.ResetClientTraffic for each one, so depleted users come back online instantly. Frontend callers (ClientsPage, useClients, ClientQrModal, ClientInfoModal, InboundInfoModal, InboundsPage, useInbounds) all switched to the new paths. The Inbounds page drops its per-inbound "Reset client traffic" and "Delete depleted clients" dropdown items — users do those at the client level now. api-docs is rebuilt to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): switch tgbot + ldap callers to ClientService Adds two thin helpers to ClientService (CreateOne, DetachByEmail) and rewrites tgbot.SubmitAddClient and ldap_sync_job to call ClientService directly. Removes the JSON-blob payloads (BuildJSONForProtocol output for add, clientsToJSON/clientToJSON helpers) that callers previously fed to InboundService.AddInboundClient/DelInboundClient. ldap_sync_job.batchSetEnable now loops InboundService.SetClientEnableByEmail per email instead of trying to coerce AddInboundClient into doing the update — the old path would have failed duplicate-email validation for existing clients anyway. The legacy InboundService.AddInboundClient/UpdateInboundClient/ DelInboundClient methods stay in place; they are now only used internally by ClientService Create/Update/Delete/Attach. Inlining + deleting them follows in a separate commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(service): move all client mutation methods to ClientService Moves the client mutation surface out of InboundService and into ClientService. These methods all operate on a single client (identity fields, traffic limits, expiry, ip limit, enable state, telegram tg id) and didn't belong on the inbound aggregate. Moved (12 methods): AddInboundClient, UpdateInboundClient, DelInboundClient, DelInboundClientByEmail, checkEmailsExistForClients, SetClientTelegramUserID, checkIsEnabledByEmail, ToggleClientEnableByEmail, SetClientEnableByEmail, ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, ResetClientTrafficLimitByEmail. Each method now takes an explicit *InboundService for the helpers that legitimately stay on InboundService (GetInbound, GetClients, runtimeFor, AddClientStat / UpdateClientStat / DelClientStat, DelClientIPs / UpdateClientIPs, emailUsedByOtherInbounds, getAllEmailSubIDs, GetClientInboundByEmail / GetClientInboundByTrafficID, GetClientTrafficByEmail). Stays on InboundService: ResetClientTrafficByEmail and ResetClientTraffic(id, email) — these mutate xray_client_traffic rows, not client identity, so they're inbound-side bookkeeping. Callers updated: tgbot (6 calls), ldap_sync_job (1 call), InboundService internal (writeBackClientSubID, CopyInboundClients, AddInbound's email-uniqueness check), ClientService Create/Update/ Delete/Attach/Detach. Also removes a dead resetAllClientTraffics controller handler whose route was already gone after the previous /clients API migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): finish migrating to ClientService + tidy IP routes Two related cleanups in the new /clients surface: 1. Move ResetAllClientTraffics (bulk-reset of xray_client_traffic + last_traffic_reset_time, with node-runtime propagation) from InboundService to ClientService. PeriodicTrafficResetJob now holds a clientService and calls j.clientService.ResetAllClientTraffics(&j.inboundService, id). The last client-mutation method on InboundService is gone. 2. Shorten redundantly-named routes/handlers under /panel/api/clients: - /clientIps/:email -> /ips/:email (handler getIps) - /clearClientIps/:email -> /clearIps/:email (handler clearIps) The "client" prefix was redundant inside the clients namespace. Frontend (InboundInfoModal) and api-docs updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds,clients): clean up inbound modal + enrich client modal Inbound modal rework (InboundFormModal.vue + inbound.js): - Drop the embedded Client subform in the Protocol tab. Multi-inbound clients are managed exclusively from the Clients page now; a fresh inbound is created with zero clients (settings constructors default to []) and the user attaches clients afterwards. - Hide the Protocol tab entirely when it has nothing to render (VMESS, Trojan without fallbacks, Hysteria). Auto-switches active tab to Basic when the tab disappears while focused. - Move the Security section (Security selector + TLS block with certs and ECH + Reality block) out of the Stream tab into its own Security tab, sharing the canEnableStream gate. Client modal additions (ClientFormModal.vue + ClientBulkAddModal.vue): - Flow select (xtls-rprx-vision / -udp443) appears only when the panel actually has a Vision-capable inbound (VLESS or PortFallback on TCP with TLS or Reality). Hidden otherwise, and cleared when it disappears. - IP Limit input is disabled when the panel-level ipLimitEnable setting is off, fetched into useClients alongside subSettings and threaded through ClientsPage to both modals. - Edit modal now shows an "IP Log" section listing IPs that have connected with the client's credentials, with refresh and clear buttons (calls the renamed /panel/api/clients/ips and /clearIps endpoints). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(inbounds): drop manual Fallbacks UI from inbound modal The PortFallback protocol type now covers the common VLESS-master-plus-children case with auto-wired dests, so the manual Fallbacks editor (showFallbacks block in the Protocol tab) is mostly redundant. Removed: - the v-if="showFallbacks" template block (SNI/ALPN/Path/dest/PROXY rows) - the showFallbacks computed - the addFallback / delFallback helpers - the .fallbacks-header / .fallbacks-title styles - the showFallbacks gate from hasProtocolTabContent (so Trojan-over-TCP no longer shows an empty Protocol tab) Power users who need a non-inbound fallback dest (nginx, static site) can still author settings.fallbacks via the Advanced JSON tab. * feat(clients,inbounds): move search/filter to Clients page + small fixes Search/filter relocation: - Remove the search/filter toolbar (search switch + filter radio + protocol/node selects + the visibleInbounds projection + inboundsFilterState localStorage + filter CSS + the SearchOutlined/ FilterOutlined/ObjectUtil/Inbound imports it required) from InboundList. The filters were all client-oriented buckets bolted onto the inbound row. - Add a search/filter toolbar to ClientsPage with the same shape: switch between deep-text search and bucket filter (active / deactive / depleted / expiring / online) + protocol filter that matches clients attached to at least one inbound with the chosen protocol. State persists in clientsFilterState localStorage. filteredClients drives both the desktop table and the mobile card list, and select-all / allSelected / someSelected only span the visible subset. - useClients now also fetches expireDiff and trafficDiff from /panel/setting/defaultSettings (used to detect the expiring bucket); ClientsPage threads them into the client-bucket helper. Loose fixes folded in: - Add Client: email field is auto-filled with a random handle on open, matching uuid/subId/password/auth. - Inbound clone: parse and reuse the source settings JSON (with clients reset to []) instead of building a fresh defaulted Settings, so VLESS Encryption/Decryption and other non-client fields survive the clone. - en-US.json: add the ipLog string used by the edit-client modal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): add Reverse tag field for VLESS-attached clients Mirrors the Flow field's pattern: a Reverse tag input appears in the Add/Edit Client modal whenever at least one selected inbound is VLESS or PortFallback. The value rides over the wire as client.reverse = { tag: '...' } so it lands directly in model.Client's *ClientReverse field; an empty value omits the reverse key entirely. On edit the field is hydrated from props.client.reverse?.tag, and the showReverseTag watcher clears the field if the user drops the last VLESS-like inbound from the selection. * fix(xray): emit only protocol-relevant fields per client entry The Xray config synthesizer was writing every identifier field (id, password, flow, auth, security/method, reverse) on every client entry regardless of the inbound's protocol. Xray ignores unknown fields, so the config worked, but it diverged from the spec and leaked secrets across protocols when one client was attached to multiple inbounds — a VLESS inbound's generated config carried the same client's Trojan password and Hysteria auth alongside its uuid. Switch on inbound.Protocol when building each entry: - VLESS / PortFallback: id, flow, reverse - VMess: id, security - Trojan: password, flow - Shadowsocks: password, method - Hysteria / Hysteria2: auth email is emitted for every protocol. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): restore auto-disable kick under new schema disableInvalidClients still resolved (inbound_tag, email) pairs via JSON_EACH(inbounds.settings.clients), which is empty after migrating to the clients + client_inbounds tables. Result: xrayApi.RemoveUser never ran for depleted clients, clients.enable stayed true so the UI showed them as active, and only xray_client_traffic.enable got flipped - making "Restart Xray After Auto Disable" only half-work. Resolve the targets via a JOIN through the new schema, flip clients.enable so the Clients page reflects the state, and drop the legacy JSON write-back plus the subId cascade workaround (email is unique now). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): live WebSocket updates + Ended status surfacing ClientsPage now subscribes to traffic / client_stats / invalidate WebSocket events instead of polling /onlines every 10s. Per-row traffic counters refresh in place, online state stays current, and list-level mutations elsewhere trigger a refresh. The client roll-up summary moves from InboundsPage to ClientsPage where it belongs, restructured into six labeled stat tiles (Total / Online / Ended / Expiring / Disabled / Active) with email popovers on the ones with issues. Auto-disabled clients (traffic exhausted or expiry passed) now classify as 'depleted' even though clients.enable=false, so they show up under the Ended filter and render a red Ended tag instead of looking indistinguishable from an operator-disabled row. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): per-node client roll-up and panel version Added transient inboundCount / clientCount / onlineCount / depletedCount fields to model.Node, populated by NodeService.GetAll via aggregated queries (one join across inbounds + client_inbounds, one over client_traffics intersected with the in-memory online emails). The Nodes list renders these as colored chips on a new "Clients" column so an operator can see at a glance how many users each node carries and how many are currently online or depleted. Also exposes the remote panel's version. The central panel adds panelVersion to its /api/server/status payload (sourced from config.GetVersion). Probe reads that field and persists it on the node row, mirroring how xrayVersion already flows. NodesPage gets a new column next to Xray Version, in both desktop and mobile views, with English and Persian strings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): stop node sync from resurrecting deleted clients Several related issues around node-managed clients: - Remote runtime: drop the per-inbound resetAllClientTraffics path and point traffic/onlines/lastOnline fetches at the new /panel/api/clients/* routes. - Delete from master: always push the updated inbound to the node even when the client was already disabled or depleted, so the node actually loses the user instead of silently keeping it. - setRemoteTraffic: mirror remote clients into the central tables only on first discovery of a node inbound. Matched inbounds let the master own the join table, so a stale snap can no longer re-create a ClientRecord (and join row) for a client that was just deleted on the master. - ClientService.Delete: route through submitTrafficWrite so deletes serialize with node traffic merges, and switch the final ClientRecord delete to an explicit Where("id = ?") clause. - setRemoteTraffic UNIQUE-constraint fix: use clause.OnConflict on inserts and email-keyed UPDATEs for client_traffics, so mirroring a snap doesn't trip the unique email index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(clients): switch client API endpoints from id to email All client-scoped routes now use the unique email as the path key (get, update, del, attach, detach, links). Email is the stable, protocol-independent identifier — UUIDs don't exist for trojan or shadowsocks, and internal numeric ids leaked panel implementation detail into the public API. Removed the redundant /traffic/byId/:id endpoint (covered by /traffic/:email) and collapsed /links/:id/:email into /links/:email, which now returns links across every attached inbound for the client. Frontend selection, bulk delete, and toggle state are now keyed by email as well, dropping the id→email lookup workaround. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(server): move cached state and helpers into ServerService ServerController had grown to hold its own status cache, version-list TTL cache, history-bucket whitelist, and the loop that drove all three — concerns that belong in the service layer. Pull them out: - lastStatus + the @2s refresh become ServerService.RefreshStatus and ServerService.LastStatus; the controller's cron now just orchestrates the cross-service side effects (xrayMetrics sample, websocket broadcast). - The 15-minute Xray-versions cache (with stale-on-error fallback) moves into ServerService.GetXrayVersionsCached, collapsing the controller handler to a single call. - The freedom/blackhole outbound-tag walk used by /xraylogs becomes ServerService.GetDefaultLogOutboundTags. - The allowed-history-bucket whitelist moves to package-level service.IsAllowedHistoryBucket, so both NodeController and ServerController validate against the same list. Net result: web/controller/server.go drops from 458 to 365 lines and contains only HTTP wiring + presentation-y side effects. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(api): emit JSON-text columns as nested objects Inbound, ClientRecord, and InboundClientIps store settings / streamSettings / sniffing / reverse / ips as JSON-text in the DB. The API was passing that text through verbatim, so every consumer had to JSON.parse a string inside a string. Add MarshalJSON / UnmarshalJSON so the wire format is a real nested object, while still accepting the legacy escaped-string shape on write. Frontend dbinbound.js gets a matching coerceInboundJsonField helper for the same dual-shape read path, and inbound.js toJson stops emitting empty/placeholder fields (externalProxy [], sniffing destOverride when disabled, etc.) so the new normalised JSON stays terse. api-docs and the inbound-clone path are updated to the new shape. Controller route lists are regrouped so all GETs sit above POSTs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): include inboundIds and traffic in /clients/list ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): gate IP Log on ipLimitEnable + clean access-log dropdown Legacy panel hid the IP Log section when access logging was off; the Vue 3 migration left it gated on isEdit only, so the section showed even when xray's access log was 'none' and nothing was being recorded. Restore the ipLimitEnable gate on the edit modal's IP Log form-item. While here, clean up the Xray Settings access-log dropdown: previously two 'none' entries appeared (an empty value labelled with t('none') and the literal 'none' from the options array). Drop the empty option for access log (the literal 'none' covers it) and relabel the empty option for error log / mask address to t('empty') so they're distinguishable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(nodes): route per-client ops through node clients API + orphan sweep Adds Runtime methods AddClient, UpdateUser, and DeleteUser so master mutates clients on a node via /panel/api/clients/{add,update,del} rather than pushing the whole inbound. The previous rt.UpdateInbound path made the node DelInbound+AddInbound on every single-client change, briefly cycling every other user on the same inbound. DelInbound no longer filters by enable=true, so a disabled node inbound actually gets removed from the node instead of being resurrected by the next snap. setRemoteTrafficLocked now sweeps any ClientRecord with zero ClientInbound rows after SyncInbound rebuilds the attachments, which is how a node-side delete propagates back to master instead of leaving a detached ghost. ClientService.Delete tombstones the email first so a snap arriving mid-delete can't re-create the record. WebSocket broadcasts an "invalidate(clients)" message on every client mutation so the Clients page refreshes without manual reload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(balancers): allow fallback on all strategies + feed burstObservatory from random/roundRobin Drops the random/roundRobin gate on the Fallback field in BalancerFormModal so every strategy can pick a fallback outbound. syncObservatories now feeds burstObservatory from leastLoad + random + roundRobin balancers (was leastLoad only), matching how leastPing feeds observatory. Fix the JsonEditor "Unexpected end of JSON input" that appeared when switching a balancer between leastPing and another strategy: the obsView watcher was gated on showObsEditor (a boolean OR of the two flags) and missed the case where one observatory swapped for the other in the same tick. Watch the individual flags instead so obsView flips to the surviving editor and the getter stops pointing at a deleted key. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): use sortedInbounds for mobile empty-state check InboundList referenced an undefined visibleInbounds in the mobile card list's empty-state guard, throwing "Cannot read properties of undefined (reading 'length')" and breaking the entire mobile render. * feat(clients): sortable table columns Adds the same sortState / sortableCol / sortFns pattern InboundList uses, wrapping filteredClients in sortedClients so sort composes with the existing search/filter pipeline. Sortable: enable, email, inboundIds (attachment count), traffic, remaining, expiryTime; actions and online stay unsorted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(shadowsocks): generate valid ss2022 keys and per-client method for legacy ciphers The Add Client flow on shadowsocks inbounds was producing xray configs that failed to start: - 2022-blake3-* ciphers need a base64-encoded key of an exact byte length per cipher. fillProtocolDefaults was assigning a uuid-style string, which xray rejects as "bad key". Now the password is generated (or replaced if invalid) via random.Base64Bytes(n) sized to the chosen cipher. - Legacy ciphers (aes-256-gcm, chacha20-*, xchacha20-*) require a per-client method field in multi-user mode; model.Client has no Method, so settings.clients was stored without one and xray failed with "unsupported cipher method:". applyShadowsocksClientMethod now injects the top-level method into each client on add/update, and healShadowsocksClientMethods backfills it at xray-config-build time so existing inbounds heal on the next start. - xray/api.go ssCipherType switch was missing aes-256-gcm, which fell through to ss2022 path. - SSMethods dropdown now offers aes-256-gcm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): preserve ClientRecord on inbound delete + filter Attached inbounds to multi-client protocols Replace the global orphan sweep in setRemoteTrafficLocked with a per-inbound diff cleanup: only delete a ClientRecord whose email disappeared from a snap-tracked inbound (i.e. a node-side delete). Inbounds that vanished entirely from the snap (e.g. admin deleted the inbound on master) aren't iterated, so a client whose last attachment came from that inbound is now left alone instead of being deleted alongside the inbound. ClientFormModal and ClientBulkAddModal now filter the Attached inbounds dropdown to protocols that actually support multiple clients: shadowsocks, vless, vmess, trojan, hysteria, hysteria2, and portfallback (which routes through VLESS settings). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): make empty-state text readable on dark/ultra themes The "No clients yet" empty state had a hardcoded black color (rgba(0,0,0,0.45)) that vanished against the dark backgrounds. Drop the inline color, let it inherit from the AntD theme, and fade with opacity like the mobile card empty state already does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(clients): client-first tgbot add flow, tgId field, lightweight inbound options - tgbot: drop legacy per-protocol Add Client UI in favour of a client-first multi-inbound flow. New BuildClientDraftMessage / getInboundsAttachPicker let an admin pick one or more inbounds and submit a single client; per- protocol secrets are now generated server-side via fillProtocolDefaults. Drops awaiting_id/awaiting_password_tr/awaiting_password_sh state cases and add_client_ch_default_id/pass_tr/pass_sh/flow callbacks. Adds a setTGUser button + awaiting_tg_id state so the bot can set Client.TgID during Add. - clients UI: add Telegram user ID input to ClientFormModal (0 = none). Hide IP Limit field entirely when ipLimitEnable is off — disabled fields still take layout space, this collapses Auth(Hysteria) to full width. - inbounds API: new GET /panel/api/inbounds/options that returns just {id, remark, protocol, port, tlsFlowCapable}. Used by the clients page pickers so the dropdown payload stays small on panels with thousands of clients (drops settings JSON, clientStats, streamSettings). Server-side TlsFlowCapable mirrors Inbound.canEnableTlsFlow so the modal no longer needs to parse streamSettings client-side. - clientInfoMsg now shows attached inbound remarks, and getInboundUsages reports the attached client count per inbound. - api-docs: document the new /options endpoint and add tgId / flow to the clients add/update bodies. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): keep Node column visible for node-attached inbounds The Node column was bound to hasActiveNode, so disabling every node hid the column even when inbounds were still attached to those nodes — the admin lost the visual cue that those inbounds belonged to a node and would come back when it was re-enabled. Combine hasActiveNode with a new hasNodeAttachedInbound check (any dbInbound with nodeId != null) so the column survives node-disable. * fix(api-docs): accept functional-component icons in EndpointSection AntD-Vue icons (SafetyCertificateOutlined, etc.) are functional components, so the icon prop's type: Object validator was rejecting them with a "Expected Object, got Function" warning at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test: cover crypto, random, netsafe, sub helpers, xray equals, websocket hub, node service Adds ~110 unit tests across previously untested packages. Focus on pure-logic and concurrency surfaces where regressions would silently affect users: - util/crypto, util/random: password hashing round-trip, ss2022 key generation, alphabet/length invariants. - util/netsafe: IsBlockedIP edge cases, NormalizeHost validation, SSRF guard with AllowPrivate context bypass. - util/common, util/json_util: traffic formatter, Combine nil-skip, RawMessage empty-as-null and copy-on-unmarshal. - sub: splitLinkLines, searchKey/searchHost, kcp share fields, finalmask normalization, buildVmessLink round-trip. - xray: Config.Equals and InboundConfig.Equals field-by-field, getRequiredUserString/getOptionalUserString type checks. - web/websocket: hub registration, throttling, slow-client eviction, nil-receiver safety, concurrent register/unregister. - web/service: NodeService.normalize validation, normalizeBasePath, HeartbeatPatch.ToUI mapping. - web/job: atomicBool concurrent set/takeAndReset semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(clients): replace English fallbacks with proper translation keys Pulls every hard-coded English label/title in the Clients page and its four modals through the i18n layer so localized panels stop leaking English. New keys live under pages.clients (auth, hysteriaAuth, uuid, flow, flowNone, reverseTag, reverseTagPlaceholder, telegramId, telegramIdPlaceholder, created, updated, ipLimit) plus refresh at the root and toasts.bulkDeletedMixed / bulkCreatedMixed for partial-failure toasts. Also switches the add-client modal's primary button from "Add" to "Create" for consistency with other create flows. The bulk-add Random/Random+Prefix/... email-method options stay hard-coded by request - they're identifier-shaped strings. * i18n: backfill 99 missing keys across all 12 non-English locales Brings every translation file up to parity with en-US.json so the Clients page, the fallback-children inbound section, the new refresh verb, the Nodes panel-version label and a handful of older holes stop falling through to the English fallback. New strings span: - pages.clients.* (labels, confirmations, toasts, emailMethods) - pages.inbounds.portFallback.* (Reality fallback inbound section) - pages.nodes.panelVersion, menu.clients, refresh Technical identifiers (Auth, UUID, Flow, Reverse tag) are intentionally left untranslated since they correspond to xray-core field names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: drop stale pages.client block duplicated in every non-English locale Every non-English locale carried a pages.client (singular) section with 30 entries that duplicated pages.clients (plural). The plural namespace is what the Vue code actually consumes; the singular one was dead weight from an older rename that never got cleaned up in the non-English files. Removing it brings every locale to exactly 984 keys, matching en-US.json. * chore: apply modernize analyzer fixes across codebase Mechanical replacements suggested by golang.org/x/tools/.../modernize: strings.Cut/CutPrefix/SplitSeq, slices.Contains, maps.Copy, min(), range-over-int, new(expr), strings.Builder for hot += loops, reflect.TypeFor[T](), sync.WaitGroup.Go(), drop legacy //+build lines. * feat(database): add PostgreSQL as an optional backend alongside SQLite Lets operators with large client counts or multi-node setups pick PostgreSQL at install time without breaking the existing SQLite default. Backend is selected at runtime via XUI_DB_TYPE/XUI_DB_DSN, a small dialect layer keeps the five JSON_EXTRACT/JSON_EACH queries portable, and a new `x-ui migrate-db` subcommand copies SQLite data into PostgreSQL in FK-aware order. * fix(inbounds): gate node selector to multi-node-capable protocols Hide the Deploy-To selector and clear nodeId when switching to a protocol that can't run on a remote node. Also: - subs: return 404 (not 400) when subId matches no inbounds, so VPN clients distinguish "deleted/unknown" from a server error - hysteria link gen: use the inbound's resolved address so node-managed inbounds advertise the node host instead of the central panel - shadowsocks: default network to 'tcp' (udp was causing issues for some clients on first-create) - vite dev proxy: rewrite migrated-route bypass against the live base path instead of a hardcoded single-segment regex Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form Bulk add/delete were serial on the frontend (one toast per call, N round-trips) and the backend race exposed by parallelizing them lost client attachments and hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also had no Start-After-First-Use option, and the table never showed the delayed duration. Backend (web/service/client.go): - Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on the same inbound don't lose the read-modify-write of settings JSON. - SyncInbound skips create+join when the email is tombstoned so a concurrent maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn- Settings) that did a stale RMW can't resurrect a just-deleted client with a fresh id. - compactOrphans sweeps settings.clients entries whose ClientRecord no longer exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each user-initiated mutation self-heals the inbound's settings. - DelInboundClient uses Pluck instead of First for the stats lookup so a missing row doesn't abort the delete with a noisy ErrRecordNotFound log. Frontend: - HttpUtil.{get,post} accept a silent option that suppresses the auto-toast. - ClientBulkAddModal fires creates in parallel + silent + one summary toast. - useClients.removeMany runs deletes in parallel + silent and refreshes once; ClientsPage bulk delete uses it and shows one aggregate toast. - useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket invalidate events from the backend collapses into a single refresh. - ClientsPage pagination is reactive (paginationState ref + tablePagination computed); onTableChange persists page-size and page changes. - ClientFormModal gains a Start-After-First-Use switch + Duration days input alongside the existing Expiry Date picker; on edit-mode open a negative expiryTime is decoded back to delayed mode + days; on submit the payload sends -86400000 * days or the absolute timestamp. - ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip Start After First Use: Nd) instead of infinity. - Telegram ID field in the form is hidden when /panel/setting/defaultSettings reports tgBotEnable=false; Comment then fills the row. - Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4) when ipLimitEnable is on, else UUID + Total GB at 12/12. - useInbounds.rollupClients counts only clients with a matching clientStats row, so orphans in settings.clients no longer inflate the inbound's count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(windows): clean shutdown, working panel restart, harden kernel32 load Three Windows-specific issues addressed: 1. Orphaned xray-windows-amd64 after VS Code debugger stop. Delve's "Stop" sends TerminateProcess to the Go binary, which is uncatchable — our signal handlers never run, so xrayService.StopXray() is skipped and xray is left dangling. Spawn xray as a child of a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS kills xray when our handle to the job is closed (which happens even on TerminateProcess). Also trap os.Interrupt in main so Ctrl+C in the terminal runs the graceful path. 2. /panel/setting/restartPanel logged "failed to send SIGHUP signal: not supported by windows" because Windows can't deliver arbitrary signals. Add a restart hook in web/global; main registers it to push SIGHUP into its own signal channel, and RestartPanel calls the hook before falling back to the (Unix-only) signal path. Same restart-loop code runs in both cases. 3. util/sys/sys_windows.go now uses windows.NewLazySystemDLL so the kernel32.dll resolve is pinned to %SystemRoot%\System32 (prevents DLL hijacking by a planted DLL next to the binary). Local filetime type replaced with windows.Filetime, and the unreliable syscall.GetLastError() fallback replaced with a type assertion on the errno captured at call time. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(sys): correct CPU/connection accounting on linux + darwin util/sys/sys_linux.go: - GetTCPCount/GetUDPCount were counting the column header row in /proc/net/{tcp,udp}[6] as a connection, inflating the reported total by 1 per non-empty file (so the panel status line always showed 2 more connections than actually existed). Replace getLinesNum + safeGetLinesNum with a single bufio.Scanner-based countConnections that skips the header. - CPUPercentRaw now opens HostProc("stat") instead of a hardcoded /proc/stat so HOST_PROC overrides apply, matching the connection counters in the same file. - Simplify CPU field unpacking: pad nums to 8 once instead of guarding every assignment with a len check. util/sys/sys_darwin.go: - Fix swapped idle/intr indices on kern.cp_time. BSD CPUSTATES order is user, nice, sys, intr, idle (CP_INTR=3, CP_IDLE=4) — gopsutil's cpu_darwin_nocgo.go reads the same layout. The previous code used out[3] as idle and out[4] as intr, so busy = total - dIdle was actually subtracting interrupt time, making the panel report CPU usage close to 100% on macOS regardless of actual load. - Collapse the per-field delta math into a single loop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(xray): rotate crash reports into log folder, prevent overwrites writeCrashReport had two flaws: it wrote to the bin folder (alongside the xray binary) which conflates artifacts, and the second-precision timestamp meant a tight restart-loop crash burst overwrote prior reports. Write to the log folder with nanosecond precision and keep the last 10 reports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(inbounds): drop unreleased portfallback protocol The Port-with-Fallback inbound (commit 62fd9f9d) was confusing as a standalone protocol — fallbacks belong on a regular VLESS/Trojan TCP-TLS inbound, the way Xray models them natively. Rip out the entire feature cleanly (no migration needed since it was never released): protocol constant, fallback children DB table, FallbackService, 2 API endpoints, all UI rows, related translations and api-docs. A native fallback flow attached to VLESS/Trojan TCP-TLS/Reality will land in a follow-up commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales. * docs: rewrite CONTRIBUTING with full local-dev setup The prior three-line CONTRIBUTING left newcomers guessing at every non-trivial step: which Go / Node versions, where xray comes from, why the panel goes blank when XUI_DEBUG=true is flipped on, how the Vue multi-page setup is wired, what to do on Windows when go build trips on the CGo SQLite driver. Now covers prerequisites, MinGW-w64 install on Windows (niXman builds or MSYS2), one-shot first-time setup, two frontend dev workflows with the XUI_DEBUG asset-cache gotcha called out, the architecture and conventions of the Vue side, a project-layout map, useful env vars, and the PR checklist. ---------
2026-05-19 10:16:42 +00:00
"usePreset": "テンプレートを使用",
"dnsPresetTitle": "DNSテンプレート",
"dnsPresetFamily": "ファミリー",
"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": "✅ Telegramユーザーが保存されました。",
"loginSuccess": "✅ パネルに正常にログインしました。\r\n",
"loginFailed": "❗️ パネルのログインに失敗しました。\r\n",
"2faFailed": "2FAエラー",
"report": "🕰 定期報告:{{ .RunTime }}\r\n",
"datetime": "⏰ 日時:{{ .DateTime }}\r\n",
"hostname": "💻 ホスト: {{ .Hostname }}\r\n",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"version": "🚀 X-UI バージョン:{{ .Version }}\r\n",
"xrayVersion": "📡 Xray バージョン: {{ .XrayVersion }}\r\n",
"ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
"ipv4": "🌐 IPv4: {{ .IPv4 }}\r\n",
"ip": "🌐 IP: {{ .IP }}\r\n",
"ips": "🔢 IP:\r\n{{ .IPs }}\r\n",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"serverUpTime": "⏳ サーバー稼働時間:{{ .UpTime }} {{ .Unit }}\r\n",
"serverLoad": "📈 サーバー負荷:{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
"serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
"tcpCount": "🔹 TCP: {{ .Count }}\r\n",
"udpCount": "🔸 UDP: {{ .Count }}\r\n",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"traffic": "🚦 トラフィック:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
"xrayStatus": " ステータス: {{ .State }}\r\n",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"username": "👤 ユーザー名:{{ .Username }}\r\n",
"reason": "❗️ 理由:{{ .Reason }}\r\n",
"time": "⏰ 時間:{{ .Time }}\r\n",
"inbound": "📍 インバウンド: {{ .Remark }}\r\n",
"port": "🔌 ポート: {{ .Port }}\r\n",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"expire": "📅 有効期限:{{ .Time }}\r\n",
"expireIn": "📅 残り時間:{{ .Time }}\r\n",
"active": "💡 有効:{{ .Enable }}\r\n",
"enabled": "🚨 有効化済み:{{ .Enable }}\r\n",
"online": "🌐 接続ステータス:{{ .Status }}\r\n",
"lastOnline": "🔙 最終オンライン: {{ .Time }}\r\n",
"email": "📧 メール: {{ .Email }}\r\n",
"upload": "🔼 アップロード: ↑{{ .Upload }}\r\n",
"download": "🔽 ダウンロード: ↓{{ .Download }}\r\n",
"total": "📊 合計: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"TGUser": "👤 Telegramユーザー{{ .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\nIDを入力してください。",
"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": "👤 1人の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": "⚙️📧 メール",
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
"change_comment": "⚙️💬 コメント",
"change_flow": "⚙️🚦 Flow",
Vue3 migration (#4198) * docs(migration): Phase 1 inventory — Vue 2 / AD-Vue 1 surface area Captures the breakage surface for the Vue 3 + Ant Design Vue 4 + Vite migration: 17,650 lines across 69 templates, 3,145 a-* component instances across 63 files, with per-pattern counts and file lists. Key findings: - No Vue filters anywhere — dodges a major Vue 3 breaking change - 358 v-model uses; AD-Vue 4 absorbs most, custom components don't - 233 <template slot="X"> usages must become <template #X> - 49 scopedSlots: { ... } column defs need new slots: { ... } shape - a-icon is removed in AD-Vue 4 — every icon must be imported Establishes the 8-phase order; Phase 2 (Vite toolchain) is next. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): Phase 2 — scaffold Vite + Vue 3 + AD-Vue 4 Adds a frontend/ directory that lives alongside the legacy web/html/ Vue 2 templates during the migration. Vite builds into ../web/dist/ so the Go binary will be able to embed the result via embed.FS once Phase 4 starts moving real pages over. - package.json pins Vue 3.5, Ant Design Vue 4.2, Vite 6, vue-i18n 10 - vite.config.js: dev server on :5173 with API proxy to the Go panel on :2053; build output to ../web/dist/ - src/App.vue is currently a smoke-test placeholder — delete once the first real page (login) lands in Phase 4 - node_modules and dist are already ignored at repo root To verify locally: cd frontend && npm install && npm run dev Pages will be migrated one at a time on the vue3-migration branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): Phase 3 — port utils, models, axios, websocket as ES modules Ports the framework-agnostic JS from web/assets/js/ into frontend/src/ so Vue 3 pages can import what they need without relying on script-tag globals. - web/assets/js/util/index.js (927 lines, 21 classes) → frontend/src/utils/legacy.js + a barrel at utils/index.js. All classes are now named exports. - Vue.prototype.$message in HttpUtil → direct import of `message` from ant-design-vue (Vue 3 has no Vue.prototype). - RandomUtil.randomShadowsocksPassword previously defaulted to SSMethods.BLAKE3_AES_256_GCM from inbound.js, creating a circular import. Replaced with the literal string default. - MediaQueryMixin (Vue 2 mixin) removed. Replaced by composables/useMediaQuery.js — Vue 3 composable returning reactive `isMobile`. - axios-init.js wrapped as setupAxios(); Qs global → npm `qs`. - websocket.js exported as WebSocketClient class; the implicit window.wsClient global is gone — pages instantiate it themselves. - model/{inbound,outbound,dbinbound,setting,reality_targets}.js copied with `export` added on every top-level declaration. Imports between models and utils are wired up explicitly. - subscription.js deferred to Phase 5 (it's a Vue 2 mount, not a util). - App.vue smoke test exercises SizeFormatter / RandomUtil / Wireguard / useMediaQuery so the user can verify Phase 3 with `npm run dev`. Run `cd frontend && npm install && npm run dev` — qs was added so a fresh install is required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 4 — port login.html to Vue 3 + AD-Vue 4 + Vite 8 First real page in the new toolchain. Multi-page Vite: each migrated page is its own entry. login.html now lives at frontend/login.html with a thin entrypoint at frontend/src/login.js mounting LoginPage.vue. Vite 6 → Vite 8.0.11 (per user request). Requires Node 20.19+ or 22.12+. @vitejs/plugin-vue bumped to ^6.0.6 (peers vite ^8). Ant Design Vue stays on 4.2.6 — there is no AD-Vue 6. Vue 2 → Vue 3 / AD-Vue 1 → AD-Vue 4 syntax changes hit on this page: - new Vue({ el, delimiters, data, methods }) → createApp + <script setup> - mounted() → onMounted() - <template slot="X"> → <template #X> - <a-icon slot="prefix" type="user"> → <template #prefix><UserOutlined /> </template> with explicit @ant-design/icons-vue imports - v-model.trim → v-model:value (AD-Vue 4 uses named v-model on inputs) Three legacy features deferred so Phase 4 stays small: - i18n (Phase 7 wires up vue-i18n) - theme switcher (custom component pending Phase 5) - headline word-cycle animation (purely aesthetic) Run `cd frontend && npm install && npm run dev`, open http://localhost:5173/login.html. With Go panel running on :2053 the form submits real credentials via the configured proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5a — theme system + Vite 8 + vue-i18n 11 Bumps Vite to 8.0.11 (npm install picked up 6.4.2 from the stale lockfile; clean install resolves the new constraint). Bumps vue-i18n to 11.1.4 since v10 was just EOL'd. Migrates aThemeSwitch.html — the two-flavor theme picker + global themeSwitcher object — into: - composables/useTheme.js: single reactive `theme` state with toggleTheme / toggleUltra. Boot side-effect applies the stored theme to <body>/<html> before Vue renders; watchEffect persists changes back to localStorage. - components/ThemeSwitch.vue: full menu version for the main panel. - components/ThemeSwitchLogin.vue: login-popover version. AD-Vue 1 → 4 changes hit on this component: - <a-icon type="bulb" :theme="filled|outlined"> dropped — replaced by explicit BulbFilled / BulbOutlined imports from @ant-design/icons-vue, swapped via <component :is="BulbIcon"> - Vue.component('a-theme-switch', { ... }) global registration → SFC + per-page import - this.$message.config(...) (Vue 2 instance method) → message.config(...) imported from ant-design-vue, called once in login.js at boot Login page now surfaces a settings button → popover → theme picker. Known gap: web/assets/css/custom.min.css isn't yet imported into the new bundle, so toggling dark mode currently only re-themes AD-Vue's own components, not the panel chrome. The body class is still toggled so behavior is correct; visual fidelity returns when custom.css is ported or directly imported. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5b — port four shared components to Vue 3 CustomStatistic.vue and SettingListItem.vue are mechanical Vue.component → SFC ports. AppSidebar.vue: AD-Vue 4 dropped <a-icon :type="dynamic">, so the five sidebar icons (dashboard/user/setting/tool/logout) live in a name→component map and render via <component :is>. The legacy <a-drawer slot="handle"> hack is replaced with a sibling fixed- position toggle button. Tab paths take basePath/requestUri as props instead of pulling them from Go template scope. TableSortable.vue: the biggest Vue 3 rewrite of this phase. - $listeners is gone — replaced by inheritAttrs: false + explicit attrs forwarding - scopedSlots: this.$scopedSlots collapsed into Vue 3's unified slots object — just iterate Object.keys(this.slots) and forward - Vue 2 h(tag, { props, on, scopedSlots }, children) → Vue 3 h(tag, { ...props, ...on }, slotsObject) - 'a-table' string → resolveComponent('a-table') so app.use(Antd) registration is honored - inject: ['sortable'] (Options API) → inject('sortable', null) (Composition API) inside the trigger child - beforeDestroy → beforeUnmount - customRow's return shape flattened (no nested props/on/class) Two intentional skips, documented in the migration doc: - aClientTable.html — slot fragments, not a component. Migrates inline with inbounds.html (new Phase 5f). - aPersianDatepicker.html — wraps a Persian-only third-party lib; defer until settings.html lands. Build verified with vite 8.0.11. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): anchor Vite dev proxy so /login.html isn't forwarded The /login proxy entry was matching any path starting with /login — including /login.html, which Vite is supposed to serve itself. Without the Go backend running, this caused ECONNREFUSED noise on every page load. Switched to regex patterns anchored with ^...$ so only the bare backend paths (/login, /logout, /getTwoFactorEnable) and explicit sub-routes (/panel/*, /server/*) get proxied. Static .html files Vite serves directly are no longer matched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): real dark mode + silence dev proxy ECONNREFUSED noise Two issues from running login.html against no Go backend: 1. Dark mode toggled the body class but didn't actually re-theme any AD-Vue components. The legacy panel relied on custom.min.css which we haven't ported. AD-Vue 4 ships its own dark algorithm — wrap LoginPage in <a-config-provider :theme="{ algorithm }"> driven by our useTheme state, and AD-Vue restyles every component for free. Page chrome (background, card, title) gets explicit .is-dark CSS since the algorithm only covers AD-Vue components. 2. Vite logged every failed proxy attempt loudly. When the Go panel isn't running locally that's pure noise. Added a configure() callback that swallows ECONNREFUSED specifically; real errors (timeouts, 5xx, anything else) still surface. Both fixes are dev-experience only — production build is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): use legacy panel palette for login page dark mode Earlier dark mode used invented colors (#141a26 page bg, #1f2937 card) that didn't match the rest of the panel. Replaced with the actual values from web/assets/css/custom.min.css: light dark ultra-dark bg #c7ebe2 bg #222d42 bg #0f2d32 card #fff card #151f31 card #0c0e12 title #008771 title #fff/.92 title #fff/.92 Drove everything off CSS custom properties on .login-app so the .is-dark / .is-ultra class swap is a few var overrides instead of duplicating selectors. Also restored the legacy card metrics (2rem radius, 4rem 3rem padding, 2rem title) so the new page matches the old panel's geometry, not just its colors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave layout + recolor for dark mode The wave SVG had inline fill="#c7ebe2" (mint) on the bottom wave, so in dark/ultra-dark mode it rendered as a pale-white blob against the dark page. Stripped the inline fills, drove them off CSS variables that swap with .is-dark / .is-ultra: light: green tints + #c7ebe2 (mint) on the bottom wave dark: #222d42 across all four waves ultra-dark: #0f2d32 The wave was also positioned wrong — anchored to the top 200px of the viewport with absolute positioning. Restored the legacy layout: - .waves-header is fixed to the top of the viewport with z-index -1 so the form floats over it - .waves-inner-header pushes the wave SVG down to ~50vh with a 50vh-tall solid block of the page color - .waves SVG itself is 15vh tall, sitting at the bottom of that block Net effect: top half is solid-colored, then a wavy edge transitions into the rest of the page, with the form centered on top — matching the legacy panel exactly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): bring wave-header to front so the wave actually shows Two layering bugs were hiding the wave entirely: 1. .ant-layout-content had background: var(--bg-page) which painted an opaque rectangle covering the full content area — including the fixed wave-header behind it. Made the layout/content transparent and moved the bg paint up to .login-app (the outer ant-layout). 2. .waves-header had z-index: -1 which on its own was fine, but with .ant-layout-content opaque on top it was doubly buried. Promoted the wave-header to z-index: 0 and gave the form .login-row z-index: 1, so the form sits above the wave and the wave sits above the page-bg. Also set --bg-page to the legacy mint (#c7ebe2) for light mode so the bottom half of the page below the wave matches the legacy panel (was white). Dark mode stays at the surface-100/login-wave palette. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): match legacy wave animation timings + dark page bg Two reasons the bottom wave looked static in dark/ultra-dark: 1. Animation durations were 7s/10s/13s/20s. Legacy uses 4s/7s/10s/13s. The 20s on the bottom wave was so slow that against the low dark- mode contrast it read as motionless. Restored the legacy timings. 2. --bg-page in dark mode was #151f31 (card color / surface-100), but the legacy .under uses surface-200 (#222d42) — that's the color of the bottom half of the page, the same as the wave fill, so the wave appears to flow into the page rather than meeting a hard edge. Now it does. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): restore Hello/Welcome headline cycle on login Earlier I deferred the legacy headline word-cycling animation as "purely aesthetic". Restored it: the title now alternates between 'Hello' and 'Welcome' every 2 seconds, matching the legacy panel. The legacy implementation toggled .is-visible / .is-hidden classes on two <b> elements via setTimeout chains and DOM querying. Replaced with a reactive ref + Vue 3 <Transition mode="out-in"> so the fade between words is declarative — no manual DOM manipulation, and the interval is properly cleaned up in onBeforeUnmount. The earlier "Welcome to 3x-ui" string was wrong on two counts: it should be just "Welcome", and it should be one of two cycling words with "Hello" preceding it. Ultra-dark palette already matched legacy after the prior wave timing fix; no additional changes needed there beyond the animation speeds that now also apply to ultra-dark via the shared CSS rules. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): correct dark login bg + give ultra-dark wave real contrast Two related fixes: 1. Default-dark wave-header bg was wrong. I had #0a2227, but that's the *ultra-dark* override; default dark uses --dark-color-background = #0a1222. Now the dark-mode top half is the legacy purple-blue instead of teal. 2. Ultra-dark wave fill is intentionally near-identical to its bg in the legacy palette (#0f2d32 vs #0a2227, ~5/11/11 RGB delta), which makes the wave look static even though the animation is running. Bumped --wave-fill / --wave-fill-bottom to #1f4d52 in ultra-dark only — far enough above the bg that the motion reads, while staying within the same teal hue family. Also corrected ultra-dark --bg-page back to #0f2d32 (was briefly #0c0e12, which is the card color, not the page color). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): drop ultra-dark bottom-wave seam line Last fix made the wave fill #1f4d52 in ultra-dark for both top-three waves and the bottom wave, which gave visible motion but exposed a hard horizontal line where the bottom wave's flat lower edge met the page bg (#0f2d32). The user noticed it as "the wave at the bottom not moving its like a line" — they were seeing the SVG's clipped bottom edge, not the wave itself. Solution: only the top three waves get the brighter fill (those carry the visible motion). The bottom wave reverts to #0f2d32 = --bg-page, so its flat bottom edge merges seamlessly into the page below. Net effect: motion is still visible (from waves 2 and 3), and there's no seam line at the bottom of the SVG. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-i — index.html dashboard shell Replaces the smoke-test App.vue with a real IndexPage shell so the /index.html route now boots the actual dashboard layout in Vue 3: - a-config-provider drives AD-Vue 4's dark algorithm from useTheme (same pattern as LoginPage) - AppSidebar (Phase 5b component) is wired in with basePath + requestUri props - a-spin loading state with placeholder card while we build out the rest of the page - Page palette mirrors the legacy: light #f0f2f5, dark #0a1222 (--dark-color-background), ultra-dark #21242a The 1,805-line legacy index.html is too big for one commit. Split into five sub-phases on the todo list: ii) status cards + /server/status polling, iii) xray status card, iv) logs/backup/panel-update modals, v) custom-geo section. frontend/src/App.vue and frontend/src/main.js (smoke-test scaffold) are removed — both purposes now served by IndexPage and index.js. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-ii — live status cards on the dashboard Adds the CPU / memory / swap / disk dashboard cards to IndexPage, backed by a useStatus() composable that polls /panel/api/server/status every 2 s and a Status / CurTotal model ported from the legacy inline classes in index.html. - models/status.js — Status & CurTotal classes (CurTotal exposes reactive .percent and .color computed-style getters; Status maps the API payload + xray state to color/message strings) - composables/useStatus.js — 2s polling with shallowRef so each fetch swaps the whole Status object atomically. WebSocket integration intentionally deferred — the legacy panel falls back to this same 2s polling when its websocket drops, so we ship the proven path first and add WS on top in a later sub-phase. - pages/index/StatusCard.vue — four a-progress dashboard widgets in a 2x2 grid (mobile collapses to a 1x4). CPU widget exposes a history button; the modal it opens is part of 5c-iv. - IndexPage now consumes both, plus useMediaQuery so the layout responds to viewport changes. AD-Vue 4 changes: <a-icon type="area-chart"|"history"> dropped in favor of explicit AreaChartOutlined / HistoryOutlined imports. <a-tooltip slot="title"> → <template #title>. i18n strings still hardcoded English (Phase 7 wires up vue-i18n). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iii — xray status card + stop/restart controls XrayStatusCard.vue renders the right-hand card on the dashboard: - Title with mobile-only version tag (matches the legacy collapse) - Animated badge for the running/stop/error states. The pulsing dot comes from xray-pulse keyframes (renamed from runningAnimation in legacy custom.min.css). Color rings on the badge use the legacy's per-state border-color overrides on .ant-badge-status-processing. - Error state replaces the badge with a popover that surfaces the multi-line errorMsg + a logs shortcut. - Action row at the bottom: optional logs (when ipLimitEnable), stop, restart, and version switch. IndexPage now wires: - POST /panel/api/server/stopXrayService and /restartXrayService, followed by a refresh() so the status card reflects the new state without waiting for the next poll tick - POST /panel/setting/defaultSettings to read ipLimitEnable - Stub handlers for the panel-logs / xray-logs / version-switch / cpu-history modals — those land in 5c-iv AD-Vue 4 changes hit on this card: - <a-icon type="bars|poweroff|reload|tool"> → explicit BarsOutlined / PoweroffOutlined / ReloadOutlined / ToolOutlined - <span slot="title|content"> → <template #title|#content> - The .xray-*-animation classes ship as global <style> (not scoped) so they pierce AD-Vue's internal .ant-badge-status-* DOM. i18n still hardcoded English; Phase 7 wires vue-i18n. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (a) — panel update / logs / backup modals Adds three of the six dashboard modals plus a Quick Actions card that surfaces them. The remaining three (xray logs, version picker, CPU history sparkline) ship in 5c-iv-b. - PanelUpdateModal.vue — current vs latest version, "update now" button. Confirm dialog → POST /panel/api/server/updatePanel, then poll /server/status for up to 90s until the new panel answers, then reload. - LogModal.vue — panel logs viewer. Filters: rows (10-500), level (debug/info/notice/warning/error), syslog toggle. Auto-fetches on open and on every filter change. Color-coded timestamps and levels via inline span styles. Download button writes the raw log to x-ui.log via FileManager.downloadTextFile. - BackupModal.vue — db export (window.location to /getDb) and import (FormData upload to /importDB, then panel restart + reload). - Quick Actions card surfaces Logs / Backup / Update buttons and shows an orange update badge (extra slot) when an update is available. Modal-busy pattern: long-running operations (update, import) emit a `busy` event with a tip; IndexPage flips its a-spin overlay so the user sees a loading message while the panel is restarting. AD-Vue 4 changes: - v-model on <a-modal> renamed to v-model:open - v-model on <a-input>/<a-select>/<a-checkbox> uses the named v-model:value / v-model:checked pattern - <a-icon type="..."> dropped — explicit Ant icon imports (BarsOutlined, CloudServerOutlined, CloudDownloadOutlined, DownloadOutlined, UploadOutlined, SyncOutlined) - Modal.confirm() replaces this.$confirm() since setup() has no `this` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-iv (b) — cpu-history / xray-logs / xray-version modals Wires up the three remaining dashboard buttons that were stubbed in 5c-iv (a): the CPU history button on StatusCard, the xray-logs button in XrayStatusCard's error popover and ipLimitEnable action, and the "Switch xray" button in XrayStatusCard's action footer. - Sparkline.vue: shared SVG line chart (composition-API port of the inline Vue 2 component). Per-instance gradient id avoids defs collisions between sparklines on the same page. - CpuHistoryModal.vue: bucket dropdown (2m/30m/1h/2h/3h/5h) drives GET /panel/api/server/cpuHistory/{bucket}; renders via Sparkline. - XrayLogModal.vue: rows + filter + direct/blocked/proxy checkboxes; POST /panel/api/server/xraylogs/{rows} returns access-log entries rendered as a colored HTML table; download button serializes to text. - VersionModal.vue: collapse with Xray panel (radio list of versions from getXrayVersion, install via installXray/{version}) and Geofiles panel (per-file reload + Update all). CustomGeo collapse panel is Phase 5c-v. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5c-v — custom-geo section in VersionModal Adds the third collapse panel ("Custom geo") that lets users register external geosite/geoip files referenced by routing rules via ext:<filename>:tag. Backend endpoints are unchanged. - CustomGeoSection.vue: bordered table over /panel/api/custom-geo/list with per-row edit, download (refetch), and delete actions, plus an Add button and Update-all. Lazy-loads the list when the parent collapse opens this panel — closed panels don't fetch. - CustomGeoFormModal.vue: shared add/edit form with the same alias regex (^[a-z0-9_-]+$) and URL validation as legacy. Type and alias are immutable when editing — backend rejects changes anyway. - ext:<filename>:tag value is click-to-copy via ClipboardManager. - Relative time is computed inline (no moment dep); tooltip shows the absolute timestamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-i — settings page shell + dirty tracking Adds the settings entry as a new Vite multi-page input. Lays down the shared page chrome (sidebar, save bar, restart, security alert) and the AllSetting fetch/dirty-poll lifecycle so 5d-ii through 5d-vi can drop in tab partials without re-implementing it. - settings.html + src/settings.js: third Vite entry; mounts SettingsPage. - SettingsPage.vue: page chrome with the legacy two-button save/restart bar, conf-alerts banner, and 5 a-tabs (4 always-visible + the formats tab gated on subJsonEnable || subClashEnable). Each tab body is an a-empty placeholder until 5d-ii…vi fill them in. - useAllSetting.js composable: POST /panel/setting/all on mount, mirrors the legacy 1s busy-loop dirty check via setInterval, and exposes fetchAll/saveAll. saveDisabled flips off as soon as the user diverges from the server snapshot. - restartPanel rebuilds the URL (host/port/scheme/base path) from the saved settings so users land on the new endpoint after a port or cert change. - models/setting.js: adopts the @/utils alias and a leading file-level doc — semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-ii — settings General tab Ports the panel/general partial (the largest single tab) — six collapse panels: General, Notifications, Certificates, External traffic webhook, Date and time, LDAP. - GeneralTab.vue receives the reactive AllSetting via props and binds fields directly with v-model:value; SettingsPage stays the sole fetch/save owner. - remarkModel/remarkSeparator surfaced as computed v-models that read+write the underlying single-string field (legacy stores them packed as <separator><orderedKeys>, e.g. "-ieo"). - LDAP inbound-tags select binds to a CSV ↔ array computed; inbound options come from /panel/api/inbounds/list on mount. - Language select stays cookie-based via LanguageManager and reloads on change — same UX as legacy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iii — settings Security tab + 2FA modal Ports the panel/security partial: change-credentials form and 2FA toggle. The 2FA modal is a new shared component since enabling 2FA, disabling 2FA, and changing credentials all funnel through it with slightly different copy. - TwoFactorModal.vue: 'set' flow renders a QR code + manual key + a 6-digit verifier; 'confirm' flow renders just the verifier. The parent passes a confirm(success) callback that fires only when the entered code matches the live TOTP value (otpauth lib). - SecurityTab.vue: holds the local user form (oldUsername/oldPassword/ new*), POSTs /panel/setting/updateUser, and on success force-redirects to logout. When 2FA is on, the credentials change goes through the confirm-modal first. - toggleTwoFactor leaves the switch read-only (the v-bound :checked matches AllSetting) and only flips after the modal succeeds, so cancelling out leaves state unchanged. - Adds otpauth ^9.5.1 dep (qrious was already present). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-iv — settings Telegram tab Ports the panel/telegram partial: bot enable/token/chatId/lang in the General panel, schedule/backup/login/CPU-threshold in Notifications, and proxy/API-server overrides in the third panel. All bindings live on the shared AllSetting reactive — no fetch/save logic in this tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-v — settings Subscription general tab Ports the subscription/general partial — four collapse panels covering the master enable switches, presentation/template fields, certs, and update interval. - Sub path goes through a strip-on-input + normalize-on-blur computed: legacy stripped `:` and `*` and ensured the value starts and ends with a single `/` — same here. - Both `subEnableRouting` and the announce/profile/title/support URLs are bound directly on AllSetting. - The "Subscription URI override" placeholder mirrors the legacy pattern for the manual full-URL form. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5d-vi — settings Subscription formats tab Ports the subscription/json partial — paths/URIs for the JSON and Clash formats plus the four packed-JSON sub-fields: fragment, noises, mux, and direct routing rules. - subJsonFragment / subJsonMux / subJsonNoises / subJsonRules are each a JSON string on the wire; the tab exposes their fields as computed v-models that read+write the underlying JSON. Toggling a top-level switch off resets the field to "" (matches legacy semantics). - Direct routing rules surface the IP and domain entries of the seed rule array as multi-select tag inputs; setting/removing tags edits the rules array in place rather than rebuilding it from scratch, so manually-added rules are preserved. - Tab is gated on subJsonEnable || subClashEnable in the parent (only rendered when the user actually opted into one of those formats). This closes Phase 5d — full settings page parity with the legacy panel across all five tabs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): route /panel/<route> to migrated pages in dev The sidebar links to production-style URLs like /panel/settings, but in dev that gets proxied to the legacy Go template — which fails because we haven't loaded the legacy asset chain. Add a proxy bypass so /panel and /panel/settings are served from index.html / settings.html on the Vite dev server itself. Unmigrated routes (inbounds, xray) still proxy to Go. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(csrf): expose token endpoint for SPA pages and fetch it from axios The legacy panel pages got their CSRF token from a <meta name="csrf-token"> tag rendered by Go. SPA pages built by Vite don't have that, so every unsafe (POST/PUT/DELETE) request from them was hitting CSRFMiddleware with no token and getting 403 — visible as the settings page being stuck on "Loading…" because POST /panel/setting/all failed. - web/controller/xui.go: GET /panel/csrf-token returns the session token. Lives under the xui group so checkLogin still gates it; the CSRFMiddleware on the same group is a no-op for GET. - frontend/src/api/axios-init.js: cache the token at module scope and lazy-fetch it when a non-safe request needs one. Seed from the meta tag first when present (legacy compat). On a 403 response, drop the cache and retry once — handles the case where a server restart rotated the token after the SPA loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): keep sidebar links absolute when basePath is empty The dashboard sidebar built tab keys as basePath + 'panel/...'. In dev the window-injected basePath is '' so the resulting key was a relative path like 'panel/settings'. When the browser resolved that against the current /panel/settings URL it produced /panel/panel/settings — visible as broken navigation between Dashboard and Settings. Force a leading slash so the keys are always absolute regardless of whether the host injected a basePath. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-i — inbounds page shell + list fetch Adds the inbounds entry as a fourth Vite multi-page input and wires /panel/inbounds through the dev proxy bypass. Lays down the page chrome (sidebar, summary statistics card, refresh button) and the fetch lifecycle composable so 5f-ii onward can drop in the table columns and the modals without re-implementing it. - inbounds.html + src/inbounds.js: fourth Vite entry; mounts InboundsPage. - InboundsPage.vue: sidebar + summary card (totals over up/down, all-time, inbound count, client tags) + a basic table with enable/ remark/port/protocol/traffic/expiry columns. Row actions, popovers, search/filter, auto-refresh, and the WebSocket delta path are all deferred to subsequent 5f subphases. - useInbounds.js composable: GET /panel/api/inbounds/list + POST /panel/api/inbounds/onlines + POST /panel/api/inbounds/lastOnline + POST /panel/setting/defaultSettings, then computes the per-inbound clientCount roll-ups (active/deactive/depleted/expiring/ online/comments) the table popovers consume. - models/dbinbound.js + models/inbound.js: switched the legacy-utils import to the @/utils alias for consistency with the rest of the app. Semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-ii — inbound list table + search/filter + auto-refresh Fleshes out the inbound list with the full column set, search & filter toolbar, row enable toggle wired to /panel/api/inbounds/setEnable/:id, and a per-row action dropdown that emits events the parent will route to modals as those land in 5f-iii through 5f-vii. - InboundList.vue (new): toolbar (Add inbound + General actions dropdown + Refresh + auto-refresh popover), search-or-filter switch with the legacy radio buttons (Active/Disabled/Depleted/Depleting/ Online), and a a-table with desktop and mobile column variants. Cells use AD-Vue 4's #bodyCell slot — protocol/clients/traffic/ allTime/expiry/info cells render the same popovers and tags as legacy. Row enable switch is optimistic with rollback on POST failure. - visibleInbounds computed mirrors the legacy search and filter projection: deep search through dbInbound + clients, or filter reduces inbound.settings.clients to the selected bucket so the table only shows matching client rows. - Auto-refresh interval is read/written to localStorage with the same keys (`isRefreshEnabled`, `refreshInterval`) as the legacy panel. WebSocket delta updates are still deferred. - Action menu emits event payloads {key, dbInbound}; the parent currently shows a "coming in later 5f subphase" toast for each. Modals (edit/qr/clone/delete/reset/info/clients) land in 5f-iii through 5f-vii. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(inbounds): wrap popover-table rows in <tbody> Vue's template compiler warned that <tr> can't be a direct child of <table> per the HTML spec; the browser silently inserts a <tbody> wrapper but Vue's SSR/hydration path doesn't, which can cause hydration mismatches. Add explicit <tbody> in both popover tables (traffic + mobile-info). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii — inbound add/edit modal + delete/clone/reset Wires up the inbound CRUD flows. The protocol-specific and transport- specific forms are still ahead in 5f-iii-b — for now the modal exposes those as JSON textareas so users can both edit existing inbounds without losing settings and create new ones from default templates. - InboundFormModal.vue: tabbed modal with a full Basics tab (enable, remark, protocol, listen, port, total GB, traffic reset, expiry date) and three JSON-edit tabs (Settings, Stream, Sniffing). Add mode stamps a fresh template per protocol via Inbound.Settings.getSettings(protocol); changing the protocol in add mode restamps the JSON. Edit mode pretty-prints the existing JSON so the user sees the same fields they save back. - POST /panel/api/inbounds/add or /panel/api/inbounds/update/:id on submit; on success the parent refreshes the list and the modal closes. Malformed JSON in any of the three textareas surfaces a message.error and aborts the save without losing user input. - InboundsPage.vue: wires the row action menu to real handlers — edit (opens the modal in edit mode), delete, reset-traffic, clone, reset-clients, del-depleted-clients all go through Modal.confirm and refresh on success. General actions menu wires reset-inbounds / reset-clients / del-depleted-clients the same way. Remaining actions (qrcode/info/import/export/copyClients) still toast as "coming soon" — those land in 5f-iv and 5f-v. - Adds dayjs ^1.11.20 dep for the a-date-picker v-model interop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iv — client add/edit + bulk-add modals Wires per-inbound client management. Both flows go through the same addClient/updateClient endpoints as legacy; the modals just funnel the form state into the right shape (`{id, settings: '{"clients": [...]}'}`). - ClientFormModal.vue: protocol-aware single-client editor — email/ password/id/auth/security/flow/subId/tgId/comment/ipLimit/totalGB/ expiry/renewal fields are shown/hidden per protocol like legacy. Edit mode displays the per-client traffic stats with a reset button; IP-limit log is read on click and clearable. Random helpers (sync icon next to each label) regenerate UUID/email/ password/sub-id values. - ClientBulkModal.vue: 1–500 clients in one POST, with the legacy five email-generation modes (Random / +Prefix / +Num / +Postfix / Pure-Prefix-Num-Postfix). Builds clients via the protocol-aware factory and concatenates their toString() output into a single settings.clients JSON array. - InboundsPage.vue: opens both modals from the row action menu (`addClient` / `addBulkClient`). They both refresh the inbound list on success. - Outstanding row actions still toast as "coming soon": qrcode, showInfo, copyClients, clipboard. Those land in 5f-v / 5f-vi. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-v — inbound info + QR-code modals Wires the row "info" and "qrcode" actions and ports the legacy inbound_info_modal end-to-end. The info modal handles every protocol the legacy panel did: • multi-user (VMess/VLess/Trojan/SS-multi/Hysteria) — per-client table + share links + per-link QR; • SS single-user — share link + QR; • WireGuard — full peer table with downloadable peer-N.conf and a wg:// share link per peer; • Mixed/HTTP/Tunnel — connection-detail tables. - QrPanel.vue: shared link card (header tag, copy button, optional download button, optional QR canvas, monospace footer with the raw value). Per-instance QRious instances are repainted on value/size change. - InboundInfoModal.vue: full info modal. Subscription URL block keys off subSettings.subURI/subJsonURI; IP-log lazy-loads on open and surfaces refresh + clear; tg-id, last-online, depleted/enabled tags all match legacy. - QrCodeModal.vue: lighter modal used for the row "qrcode" action on SS-single and WireGuard inbounds (just the QRs, no info table). - InboundsPage.vue: wires both flows. checkFallback() reproduces the legacy logic — when an inbound listens on a unix-socket fallback (`@<name>`), the link generator is pointed at the root inbound that owns the listen address so QRs/links carry the public host:port + the right TLS state. Multi-client navigation (focusing a specific client's links) is deferred to 5f-vi where the per-inbound expand- row table will pass the email through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vi — per-inbound client expand-row table Each multi-user inbound row in the list now expands to show its client roster, mirroring the legacy aClientTable component. - ClientRowTable.vue: inner a-table with full desktop column set (action icons / enable / online / client-with-status-dot / traffic with progress bar / all-time / expiry with reset cycle) and a collapsed mobile variant (single dropdown menu + popover info). Self-contained: stats are looked up via a per-inbound email->stats Map; per-client confirms (reset/delete) live on the row. - The component emits typed events (edit/qrcode/info/reset-traffic/ delete/toggle-enable) — InboundsPage routes them back to the existing client and info modals (with `findClientIndex` so the modal opens focused on the right client). - InboundList.vue: hooks ClientRowTable into the a-table's expandedRowRender slot; row-class-name `hide-expand-icon` and a scoped CSS rule hide the chevron for non-multi-user inbounds (HTTP/Mixed/Tunnel/WireGuard/SS-single) so they keep looking flat. - toggle-enable-client routes through updateClient with the same `{id, settings: '{"clients": [...]}'}` shape as the other modals, so backend parsing stays single-pathed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-iii-b — replace inbound modal JSON textareas with structured forms Rewrites InboundFormModal to look like the legacy panel: structured forms for the common case, with a compact "Advanced (JSON)" fallback for the rare bits we don't yet have UI for. Tabs: • Basics — enable/remark/protocol/listen/port/total/trafficReset/expiry • Protocol — protocol-aware: VMess/VLess/Trojan/SS-multi/Hysteria in add mode embed an inline first-client form (email + ID/password/auth, security, flow, subId, comment, total GB, expiry); edit mode shows a clients-count summary table; VLess: decryption/encryption inputs; SS: method dropdown that re-randomizes password and propagates method change to the multi-user array (matches legacy SSMethodChange); HTTP/Mixed: accounts table with add/remove rows + Mixed auth/udp/ip toggles; Tunnel: address/port/network/followRedirect; WireGuard: secretKey/pubKey (regen via Wireguard.generateKeypair) + per-peer fields with PSK regen + allowedIPs add/remove + keepAlive. • Stream — only when canEnableStream(); transport selector with structured forms for TCP (proxy-protocol, http camouflage), WS (host/path/heartbeat/headers), gRPC (serviceName, multiMode), HTTPUpgrade (host/path). KCP/XHTTP fall back to the Advanced tab with an alert banner. Security selector with TLS (sni/alpn/ fingerprint) and Reality (target/serverNames/keypair-gen via /panel/api/server/getNewX25519Cert / shortIds / fingerprint). • Sniffing — enabled/destOverride/metadataOnly/routeOnly/ ipsExcluded/domainsExcluded as structured fields. • Advanced (JSON) — raw streamSettings + sniffing JSON for users reaching KCP/XHTTP/sockopt/finalmask/full TLS cert arrays. The stream JSON is auto-synced from the live model whenever the structured fields change. State source of truth is a deeply-reactive Inbound + DBInbound pair cloned on open; submit serializes via inbound.settings.toString() + inbound.stream.toString() so the wire shape matches the legacy panel byte-for-byte. streamNetworkChange semantics (clear flow when TLS/Reality unavailable, reset finalmask.udp when not KCP) are preserved. Vision Seed for VLess + finer-grained TCP HTTP camouflage + the full TLS cert/ECH editor will land in 5f-iii-c. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 5f-vii — shared text/prompt modals + remaining export/import wiring Wires up the last batch of inbound row + general actions that were toasting "coming soon": export-inbound-links, export-subs (per-inbound and global), export-all-links, import-inbound, and the clipboard JSON peek. Two small shared components back them — both can be reused by the xray page later. - TextModal.vue (shared): read-only multi-line viewer with a copy button and an optional download button when fileName is set. Replaces the legacy txtModal which the inbounds page used for every link export. - PromptModal.vue (shared): generic title + input/textarea + confirm callback, with the legacy keybindings (Enter submits in single-line mode; Ctrl+S submits in textarea mode). Used here for import-inbound but also by xray-config edits in Phase 6. - InboundsPage.vue: drops the toast stubs for `import`/`export`/`subs` on the general-actions menu and `export`/`subs`/`clipboard` on the per-row menu, routing each through openText / openPrompt + the appropriate model helper (genInboundLinks, etc.). The copyClients cross-inbound modal stays toast-stubbed — that's its own dedicated legacy modal worth its own commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-i — xray page scaffold + Advanced JSON tab The fifth and last legacy page comes online. Tabs are scaffolded with a-empty placeholders for the structured editors (Basics / Routing / Outbounds / Balancers / DNS) so navigation is stable; the Advanced (JSON) tab is fully functional and lets power users edit the raw xraySetting tree exactly like the legacy CodeMirror pane. - xray.html + src/xray.js: fifth Vite multi-page entry, mounted as XrayPage; vite.config.js routes /panel/xray and /panel/xray/ to it through the dev proxy bypass alongside the other pages. - XrayPage.vue: page chrome with the Save / Restart-xray bar, restart- output popover (surfaces /panel/xray/getXrayResult content when startup fails), 6 a-tabs, and a textarea-backed Advanced JSON editor. CodeMirror is intentionally not pulled in — the textarea works for every modern browser and keeps the bundle slim while structured editors land in 6-ii through 6-v. - useXraySetting.js composable: POST /panel/xray/ on mount, mirrors the settings-page 1s busy-loop dirty check for both xraySetting and outboundTestUrl, and exposes saveAll + restartXray. The dirty flag relies on string equality of the pretty-printed JSON, so reformat-only edits don't enable Save. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-ii — xray Basics tab structured editor Replaces the placeholder on the Basics tab with a structured form for the most-touched fields of the xray template — outbound + routing strategy, log levels, traffic stat counters, and the "basic routing" shortcuts (block torrent / IPs / domains, direct IPs / domains, IPv4 forced, WARP / NordVPN routing). - useXraySetting.js: hoists a parsed `templateSettings` reactive alongside the JSON string, with two cooperating watches that keep them in sync. Editing structured fields stringifies into xraySetting for the dirty-poll + Advanced JSON tab; editing the JSON re-parses into templateSettings only when valid, so structured tabs stay readable mid-edit. - BasicsTab.vue: collapse panels mirror the legacy partial — General, Statistics, Logs, Basic routing. Every input is a computed v-model reading/writing into templateSettings; the routing-rule shortcuts funnel through ruleGetter/ruleSetter which match the legacy templateRuleGetter/templateRuleSetter behavior (replace-first, drop-duplicates, pop-the-rule-when-empty). Direct/IPv4 setters also call syncOutbound() to provision/prune the matching outbound. - XrayPage.vue: imports BasicsTab + derives `warpExist`/`nordExist` from the parsed templateSettings. WARP/NordVPN provisioning modals are still placeholders that toast — those land in 6-v with the routing/outbound editors. Default tab flips back to Basics so users land on the structured editor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iii — xray Routing tab + rule modal Replaces the Routing tab placeholder with a full editor for templateSettings.routing.rules: - RoutingTab.vue: a-table over the parsed rules with the legacy six- column layout (action / source / network / destination / inbound / outbound) and the same "lead value + N more" pill renderer for multi-value criteria. Mobile drops source/network/destination for readability. Per-row dropdown handles edit / move-up / move-down / delete; the array-mutation reordering replaces the legacy jQuery Sortable drag handle without pulling in a sortable lib. - RuleFormModal.vue: full form mirroring xray_rule_modal.html — CSV inputs for sourceIP/sourcePort/vlessRoute/ip/domain/user/port, Network select, Protocol multi-select, Attrs key/value pairs, inbound-tag multi-select sourced from templateSettings.inbounds + parent inboundTags + dnsTag, outbound-tag single-select sourced from templateSettings.outbounds + clientReverseTags, and balancerTag from templateSettings.routing.balancers. Submit serializes via the same shape the legacy `getResult` produces (CSV → array, drop empty fields). - XrayPage.vue: imports RoutingTab and exposes inboundTags + clientReverseTags from useXraySetting so the modal can populate its tag pools. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-iv — xray Outbounds tab + outbound modal Replaces the Outbounds tab placeholder with a full table + add/edit flow. The 1.3k-line legacy outbound modal is condensed to a tabbed modal with structured Basics fields (tag/protocol/sendThrough/domain strategy) and JSON tabs for the protocol-specific settings + stream trees — same approach the Inbound modal uses, and a power user can still edit the same trees via the page-level Advanced (JSON) tab. - useXraySetting.js: adds fetchOutboundsTraffic + resetOutboundsTraffic + testOutbound. Test states are tracked per outbound index so the row's Test button can show loading + the Test-result column can render the response delay / status / error. - OutboundsTab.vue: full table (action / identity / address / traffic / test result / test) plus a card-list mobile variant with the same row dropdown (set-first / edit / move up/down / reset traffic / delete). outboundAddresses() reproduces the legacy findOutboundAddress logic so each protocol's host:port list is rendered consistently. Add/edit go through OutboundFormModal, delete goes through Modal.confirm, reset traffic posts to /panel/xray/resetOutboundsTraffic with the row's tag (or "-alltags-" from the toolbar). - OutboundFormModal.vue: tag/protocol/sendThrough/domainStrategy on the Basics tab; settings + streamSettings as raw JSON on their respective tabs. Tag-collision check happens client-side before emitting; malformed JSON aborts the save with a message.error. - XrayPage.vue: imports OutboundsTab and wires the test action to the composable's testOutbound helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-v — xray Balancers tab + DNS placeholder Brings Balancers to full parity with the legacy panel and adds a DNS tab placeholder that exposes the full dns/fakedns trees as JSON so users can edit them without falling through to Advanced. - BalancerFormModal.vue: tag (with duplicate-tag warning across other balancers), strategy (random/roundRobin/leastLoad/leastPing), selector tag-mode multi-select sourced from existing outbound tags + free-form additions, fallback. Disable-on-invalid is driven by the duplicateTag + emptySelector computed flags. - BalancersTab.vue: empty state with a single "Add balancer" CTA; populated state shows the legacy 4-column table (action / tag / strategy / selector / fallback) with per-row edit + delete in a dropdown. On submit the wire shape preserves the `strategy: { type }` nesting only when the strategy is non-default, matching the legacy emit. Tag renames also chase across routing.rules.balancerTag references so existing rules don't dangle. - DnsTab.vue: master enable switch + raw JSON for `dns` and `fakedns`. Legacy had a dedicated server-by-server editor + a fakedns row editor; both are big enough to deserve their own commits, and the JSON path supports every field today. WARP / NordVPN provisioning modals still toast as "coming soon" — those are third-party API integrations worth their own commits. The xray page now has structured editors for Basics / Routing / Outbounds / Balancers and JSON editors for DNS / Advanced — every xray tab the legacy panel offered is functional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(server): Phase 8 — cut HTML routes over to web/dist/ Production cutover. Every user-facing HTML route now serves the Vue-3-built bundle from web/dist/ instead of rendering the legacy Go template; the long-hashed Vite assets are served at /assets/ from the same embedded filesystem. The legacy templates in web/html/ and the legacy static tree in web/assets/ are kept on disk for now in case a quick revert is needed, but nothing the binary serves references them. What changed: - web.go: a new //go:embed dist/* feeds the controller package via a SetDistFS hand-off before controller construction. The static /assets/ route is rebound: in dev to web/dist/assets/ on disk so Vite's incremental rebuilds show up live; in prod to the embedded dist via wrapDistFS (rooted one level deeper than wrapAssetsFS). - controller/dist.go: serveDistPage helper used by every HTML handler. Reads dist/<name> from the embedded FS and applies two transforms before sending: 1. injects <script>window.__X_UI_BASE_PATH__="..."</script> just before </head> so AppSidebar links resolve under the panel's basePath. 2. when basePath != "/", rewrites Vite's absolute /assets/ URLs to <basePath>assets/ so installs running under a custom URL prefix load the bundle where the static handler lives. HTML responses go out with no-cache so panel upgrades reach users on the next refresh; hashed JS/CSS stays cacheable. - controller/index.go: IndexController.index now serves dist/login.html for logged-out callers (the redirect for logged-in users is unchanged). - controller/xui.go: XUIController.{index,inbounds,settings,xraySettings} each become a one-line wrapper around serveDistPage. Smoke checklist for the maintainer: - run `cd frontend && npm run build` to refresh web/dist/ before building the Go binary (the embed snapshot is taken at compile time); - visit /panel/, /panel/inbounds, /panel/settings, /panel/xray and confirm each loads its Vue page; - log out and log back in to verify the login flow; - confirm the sidebar links navigate correctly under your install's basePath; - POST flows (e.g. saving settings) still need the CSRF token — that endpoint (/panel/csrf-token, added earlier) is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 6-vi — WARP + NordVPN provisioning modals Replaces the toast stubs on the Basics tab and Outbounds toolbar with the legacy WARP + NordVPN provisioning flows. Both modals now stage their wireguard outbounds back into templateSettings.outbounds through the same event channels OutboundsTab uses, so the existing add / reset / delete / refresh-traffic surface keeps working. - WarpModal.vue: empty state shows a single Create button that generates a wireguard keypair locally (Wireguard.generateKeypair) and posts it to /panel/xray/warp/reg; populated state surfaces the access_token / device_id / license_key / private_key, lets the user upgrade to WARP+ via /panel/xray/warp/license, refreshes the account info from /panel/xray/warp/config (plan / quota / usage in human-readable bytes), and stages a wireguard outbound with the WARP-specific reserved-byte encoding pulled from client_id. Add / Reset / Delete go through events the parent routes back to templateSettings.outbounds. - NordModal.vue: dual-tab login (NordVPN access token → /panel/xray/nord/reg, or paste a NordLynx private key → /panel/xray/nord/setKey). Once authenticated, country / city / server selectors fetch from /panel/xray/nord/{countries,servers}, servers sort by load ascending, the lowest-load server in the current city auto-selects. Reset emits oldTag/newTag so the parent renames matching routing rules in place; logout emits a remove-routing-rules event with prefix `nord-` to purge any dangling references. - XrayPage.vue: holds warpOpen / nordOpen flags, ensures the outbounds array exists before mutating it, and wires the modal events (add-outbound / reset-outbound / remove-outbound / remove-routing-rules) to in-place edits of templateSettings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): Phase 7 — vue-i18n wired up + login page translated Sets up vue-i18n on top of the panel's existing TOML translation files. The Go side stays the source of truth — translators continue to edit web/translation/*.toml; a sync script snapshots those files into per-locale JSON the Vue bundle imports. The login page is translated end-to-end as a worked example; remaining pages can be converted incrementally without infrastructure churn. What's in the box: - scripts/sync-locales.mjs: small TOML→JSON converter that walks web/translation/*.toml and writes frontend/src/locales/<code>.json. Handles the narrow subset of TOML the panel uses (flat key/value pairs + dotted [section.subsection] heads). Wired as a `prebuild` + `predev` script so production builds always include the latest strings without a manual step. - src/i18n/index.js: createI18n() in composition mode with all 13 locales emitted as their own Vite chunks. The active locale (read from the same `lang` cookie LanguageManager has always managed) plus the en-US fallback are eagerly loaded; the rest are dynamically importable via a loadLocale(code) helper. This keeps the per-page bundle the user actually downloads small — only ~30 KB of strings end up in the initial payload, vs ~220 KB if all 13 were eager. - All five page entries (index/login/settings/inbounds/xray) wire the i18n plugin into createApp via .use(i18n). - LoginPage.vue: t(...) replaces hardcoded English on the username / password / 2FA placeholders, the submit button label, and the Settings popover title. The Hello/Welcome headline cycle stays hardcoded — those are stylistic, not labels. The 'Hello'/'Welcome' cycle stays in English deliberately; the rest of the migration's components still ship hardcoded English and will be converted page by page in follow-up commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate page chrome — sidebar, save bars, tabs, summary cards Replaces hardcoded English with t() calls in the components every user sees on every page load. The translations themselves come from the existing TOML files via the sync script — no new strings, no new locale keys. Per component: - AppSidebar.vue: 5 menu titles (dashboard / inbounds / settings / xray / logout). Computed so the sidebar re-renders when the cookie-driven locale flips on reload. - IndexPage.vue: Quick actions card title + Logs / Backup / Up-to- date / Update buttons. - StatusCard.vue: CPU / Memory / Swap / Storage labels + logical-processors / frequency tooltips. - XrayStatusCard.vue: card title + error popover header + Stop / Restart / Switch xray action labels (kept the v-prefix version string as-is — it's content, not a label). - SettingsPage.vue: 5 tab titles + Save / Restart-panel buttons + unsaved-changes warning. - XrayPage.vue: 6 tab titles + Save / Restart-xray buttons + unsaved-changes warning. - InboundsPage.vue: 5 summary-stat card titles. - InboundList.vue: 10 column titles (computed for live locale), Add inbound / General actions buttons + every dropdown menu item, search placeholder, filter radio labels, popover titles (disabled / depleted / depleting / online), traffic + info popover row labels. Total: ~75 strings localised across 8 files. The remaining English labels live in the per-tab settings forms, the form modals (Inbound / Client / Outbound / Rule / Balancer / WARP / Nord), and the per-row table cell helpers — all incremental work that doesn't touch infrastructure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): translate every remaining English string on the index page Closes the index page's i18n coverage. Combined with the page-chrome commit, every label users see on the dashboard is now sourced from the TOML translation files. Per file: - IndexPage.vue: loading-spinner tip (initial + dynamic). - BackupModal.vue: modal title, both list-item titles + descriptions ("Back up" / "Restore"), in-flight busy tips ("Importing database…" / "Restarting panel…"). - PanelUpdateModal.vue: modal title, update-available alert, current/latest version row labels, "Up to date" tag + label, primary action button. Modal.confirm now uses the translated panelUpdateDialog / panelUpdateDialogDesc with #version# substitution; success toast uses panelUpdateStartedPopover. - LogModal.vue: title slot ("Logs"). The Debug/Info/Notice/Warning/ Error log-level options stay literal — they're xray's wire values, not user-facing labels (matches the existing settings-page choice). - XrayLogModal.vue: title + Filter label. Direct/Blocked/Proxy stay literal for the same reason. - VersionModal.vue: modal title + xray-switch alert + per-file tooltip + "Update all" button + custom-geo collapse header. The Modal.confirm flows for switchXrayVersion + updateGeofile use translated dialog/desc with #version# / #filename# substitution. - CpuHistoryModal.vue: title slot. - CustomGeoSection.vue: routing-hint alert, Add / Update-all buttons, every column title (computed for live locale), copy/edit/download/ delete tooltips, copy toast, delete-confirm modal, empty-state text. - CustomGeoFormModal.vue: add/edit titles, OK/cancel labels, Type/ Alias/URL field labels, alias placeholder, all three validation toasts. Total: ~50 strings localised across 8 index-page files. The Hello / Welcome login headline cycle and a handful of literal xray wire values (Direct/Blocked/Proxy/log levels) are intentionally kept hardcoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n(frontend): Phase 7-c — translate settings, inbounds modals, xray tabs Continues the page-by-page translation pass started in cb37dd55 — runs every user-visible string on settings (General/Security/Telegram/Sub), inbounds (Client/QR/Info modals), and xray (Routing/Balancer/Rule/Warp/ Nord/Basics/Outbounds tabs) through useI18n. Updates the TOML→JSON sync script to escape `@` (vue-i18n parses it as a linked-format prefix) and refreshes all 13 locale files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): Phase 9 — restore index dashboard, fix login/CSRF, port legacy styles - Index dashboard regains the 8 cards that were lost in the SPA port (3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed, Total Data, IP Addresses, Connection Stats), plus a Config button that shows the live xray config.json. Version display falls back through panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank. - Xray config no longer hangs on load: useXraySetting surfaces failures instead of leaving a perpetual spinner, and the Vite dev proxy stops hijacking POST requests to migrated routes (only GETs get bypassed). - Inbound page no longer throws __asyncLoader/emitsOptions errors — inbound.js was missing imports (NumberFormatter, SizeFormatter, Wireguard) and InboundList kept emitting after unmount. - Login round-trip works after logout: a public /csrf-token endpoint bootstraps the SPA before authentication, axios caches the token module-level, and the dev 401 handler navigates to /login.html instead of reloading the dashboard into a redirect loop. - legacy.css mirrors the legacy panel's surface/text variables so dark and ultra-dark themes match main; every SPA entry imports it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray DNS section to match main branch DnsTab now exposes every field the legacy panel did — top-level toggles (tag, hosts, queryStrategy, disableCache/queryConcurrency, fallback strategy, client subnet), the servers table with per-row strategy and domain/expectIP/unexpectedIP overrides, and the Fake DNS pool. The new DnsServerModal covers the full add/edit flow and collapses to a bare string when the user only sets an address — matching the wire shape the legacy form emits for plain DNS entries like "8.8.8.8". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): rebuild xray outbound modal with structured per-protocol forms Replaces the JSON textareas with the same shape the legacy panel uses: all 11 outbound protocols (vmess/vless/trojan/shadowsocks/socks/http/ mixed/wireguard/tun/dns/loopback/blackhole/freedom) get dedicated fields, every transport (TCP/KCP/WS/gRPC/HTTPUpgrade/XHTTP) gets its own panel, and TLS/Reality/sockopt/Mux are configured through the same controls as the inbound side. Brings the SPA outbound editor to parity with main so users no longer have to drop into raw JSON. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): bring inbound modal to full parity with main branch Switches the default protocol on add to VLESS, fixes a crash when adding a Mixed account (the constructor is SocksAccount, not MixedAccount), and fills in the fields the SPA was previously delegating to the Advanced JSON tab: - TLS: cipher suites, min/max version, reject SNI / disable system root / session resumption switches, the certificate array with per-row Path-or-Content toggle (Set Default pulls from /panel/setting/ defaultSettings), One Time Loading, Usage / Build Chain, plus ECH key/config with a Get New ECH Cert button. - Reality: xver, target/SNI sync icons (uses getRandomRealityTarget), max time diff, min/max client version, short IDs randomizer, SpiderX, mldsa65 seed/verify with Get New Seed. - Stream: full structured forms for every transport — TCP HTTP camouflage gets its request/response editor, mKCP gets MTU/TTI/uplink/ downlink/CWND/maxSendingWindow, WebSocket / gRPC (now with Authority) / HTTPUpgrade get headers + proxy-protocol toggles, XHTTP gets the full SplitHTTPConfig surface (mode-aware fields, padding obfs, session/sequence placement, uplink data, no-SSE). - New External Proxy section and a structured Sockopt block (mark, TCP keepalive/timeout/clamp, fast open, MPTCP, penetrate, V6Only, domain strategy, congestion, TProxy, dialer/interface, trusted XFF). - VLESS gets the legacy X25519 / ML-KEM-768 buttons that fetch fresh decryption/encryption blocks via /panel/api/server/getNewVlessEnc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): add FinalMask UI (TCP/UDP masks + QUIC params) to inbound and outbound Mirrors web/html/form/stream/stream_finalmask.html as a shared FinalMaskForm component used by both modals — they share the same StreamSettings shape (addTcpMask/addUdpMask/finalmask/enableQuicParams) so a single template handles both. Surfaces: - TCP masks for raw/tcp/httpupgrade/ws/grpc/xhttp networks: fragment, sudoku, and header-custom (with the 2D clients/servers groups, each row supporting array/str/hex/base64 packets and a randomize button for base64). - UDP masks for hysteria protocol or kcp network: hysteria gets just salamander; kcp gets the full type list (mkcp variants, header-*, xdns/xicmp, header-custom with flat client/server lists, and noise). Switching to xdns shrinks the kcp MTU to 900 to match the legacy panel's behavior. - QUIC Params for hysteria or xhttp: congestion (incl. brutal up/down fields), debug, UDP hop ports/interval, idle/keepalive timeouts, path-MTU discovery toggle, and the four receive-window tunables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): remove duplicate Outbound test URL from xray Advanced tab The Basics tab already exposes this field through BasicsTab — duplicating it on the Advanced tab let two inputs race the same ref and only added clutter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): unify theming on vanilla AD-Vue light/dark/ultra-dark The legacy panel CSS (custom.min.css ported as legacy.css) tinted every non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary) overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue — producing the mixed blue/green button look on dark mode. Drop legacy.css entirely and let AD-Vue 4's algorithms own the palette. Centralize antdThemeConfig in useTheme.js so every page resolves to the same source of truth (light = defaultAlgorithm, dark = darkAlgorithm, ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/ Elevated tokens). Each page's <a-config-provider> now imports the shared computed instead of defining its own copy. Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): restore computed import in Settings + Xray pages When 5f1aba28 dropped the local antdThemeConfig computed (now shared from useTheme), it also stripped `computed` from the import list — but both pages still call computed() elsewhere (confAlerts, advanced-tab helpers). Re-adds it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): retheme dashboard gauges to AD-Vue blue and shrink them - StatusCard's CPU/RAM/Swap/Storage dashboards rendered at AD-Vue's default 120px width which made the percent text balloon to ~36px. Drop to 90px (70px on mobile) so the gauge fits the rest of the card. - The CurTotal.color thresholds still hardcoded the legacy teal/orange palette (#008771 / #f37b24 / #cf3c3c). Switch to AD-Vue's primary / warning / danger tokens (#1677ff / #faad14 / #ff4d4f) so the gauges match the rest of the panel under both light and dark themes. - XrayStatusCard's running-animation badge ring also still pointed at the deleted --color-primary-100 var; hardcode the new primary blue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: shorten backupTitle to "Backup & Restore" across all 13 locales The backup modal header was the second-longest title in the dashboard on every locale ("Database Backup & Restore" / "Резервне копіювання та відновлення бази даних" / etc). Drop the "Database / Veritabanı / 数据库" qualifier — the modal already lives under the "Database" column, so the shorter form reads cleaner on narrow viewports. Updated both the .toml source-of-truth files and the synced .json locales (re-running scripts/sync-locales.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * i18n: collapse two translation databases into a single web/translation/<lang>.json set The Vue SPA had been reading from frontend/src/locales/*.json while the Go binary still loaded web/translation/translate.*.toml — and a sync-locales.mjs pre-build step kept the two in lockstep, with TOML as the source of truth. Now that go-i18n v2.6.1 already flattens nested JSON via recGetMessages/addChildMessages, both runtimes can share one file per locale. - Move the 13 nested-JSON locale files to web/translation/<lang>.json so they live alongside the Go //go:embed translation/* directive. - Switch web/locale/locale.go from toml.Unmarshal to json.Unmarshal (and drop the pelletier/go-toml import — it's now indirect-only). Confirmed via a smoke test that pages.index.cpu, subscription.title, tgbot.commands.help, and menu.settings all resolve in en-US, fa-IR, ru-RU, and zh-CN. - Repoint Vue's i18n loader at the new path (../../../web/translation/ *.json glob) and drop the moved-here pathDelimiter comment that no longer applies. - Delete the 13 legacy translate.*.toml files and the sync-locales.mjs script + its npm pre-script hooks (predev/prebuild/i18n:sync). The Telegram bot and subscription page still get their messages because they were reading the same MessageIDs the JSON files now produce. - Update copilot-instructions.md so the next contributor knows where the canonical translation files live. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): redesign expand-row + retheme client visuals When you expanded an inbound row, the nested <a-table> inside ClientRowTable burst out of the parent's scroll-x box — its .ant-spin-container ended up wider than the parent's narrow .ant-table-cell, so the child looked oversized while the parent looked squeezed. Replace the nested table with a CSS-grid layout that owns its sizing, sits flush inside the expanded cell, and collapses to a 3-column layout on mobile (action menu, client identity, info popover). While in there, fix three other client-row visuals: - The Unicode infinity glyph (U+221E) renders as an "m"-shaped character in some system fonts (Windows Segoe UI in particular). Add a shared <InfinityIcon /> SVG component (legacy panel's path) and use it in ClientRowTable, InboundList, and InboundInfoModal — desktop and mobile cells. - The "unlimited quota" traffic bar passed :percent="100" with no stroke-color, so AD-Vue auto-coloured it success-green. Pin it to the AD-Vue purple token (#722ed1) so it reads as the no-limit sentinel rather than another usage state. - ColorUtils + the in-row statsExpColor still hardcoded the legacy teal/orange/red/purple palette (#008771 / #f37b24 / #cf3c3c / #7a316f). Map them onto AD-Vue 4's success/warning/danger/purple tokens (#52c41a / #faad14 / #ff4d4f / #722ed1) so badges, tags, and progress bars all match the rest of the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): darken light-theme page bg so cards stand out The light-theme --bg-page was #f0f2f5 — close enough to AD-Vue's #fff card background that the cards faded into the page. Bump it to #e6e8ec (a more visibly distinct gray) so cards lift cleanly off the surface. Dark and ultra-dark stay where they were. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): shrink dashboard percent text and surface the unfinished arc Two follow-up tweaks to the dashboard gauges: - AD-Vue scales the percent text from the SVG, not from :width, so the 90px gauges still rendered the number at ~27px. Pin .ant-progress-text to 14px via :deep() and trim the gauge to 70px (60px on mobile) so the whole card stays compact. - The default trail (rgba(0,0,0,0.06) / rgba(255,255,255,0.08)) was invisible on the light-theme card. Pass an explicit rgba(128,128,128,0.25) trail-color so the unfinished portion is visible under both light and dark themes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): migrate subpage.html to Vue 3 SPA The subscription info page was the last page still rendered by Go templates. Move it to the Vite multi-page setup so the whole panel loads through one toolchain. Frontend: SubPage.vue mounts at /sub/<id>?html=1 and reads window.__SUB_PAGE_DATA__ for the parsed view-model (traffic / quota / expiry + rendered share links). Fix descriptions borders against the light-theme card by painting the row divider on each cell's bottom edge — AD-Vue's <tr> border doesn't render reliably under border-collapse:collapse. Backend: serveSubPage reads dist/subpage.html, injects window.__X_UI_BASE_PATH__ + window.__SUB_PAGE_DATA__ before </head>, and rewrites Vite's absolute /assets/ URLs when the panel runs under a URL prefix. Drop the legacy template-FuncMap wiring and switch the sub server's static mount from web/assets to web/dist/assets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): inbound modal QR + tabs + restored TLS fallbacks Per-client QR action: the qr icon on the expand-row table opened the big info modal instead of the QR modal. Route it to QrCodeModal and extend that modal with a `client` prop so genAllLinks() produces the per-client share URLs (and per-peer remarks for WireGuard). Inbound's Data redesign: split the dense single-page view into three tabs — Inbound, Client, Subscription. Drop every QR rendering from this modal (QrCodeModal is the QR home now). Each row in the Inbound tab is one label/value pair instead of the legacy 2-column grid, and long values like the VLESS encryption blob render as a wrapping code block with a copy button so they can't blow out the dialog. The Subscription tab renders sub URL + JSON URL as clickable anchors that open in a new tab. Restored TLS fallbacks UI: the model already exposed VLESSSettings.Fallback / TrojanSettings.Fallback with addFallback / delFallback / fallbackToJson, but the form modal never surfaced them during the Vue 3 migration. Re-add the legacy form (SNI, ALPN, Path, Destination, PROXY) on the protocol tab, gated on TCP transport plus (for VLESS) encryption=none — same conditions as main. Column widths: Protocol 70→130 and All-time Traffic 60→95 in the inbound list; All-time Traffic 90→130 in the client expand-row, so the header text fits and tags don't get squeezed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): navy dark theme + rounded inbound/client corners Dark theme picks up a refined navy palette (page #0a1426, cards #142340, sider #0d1d33) so the sidebar blends with the rest of the surface; ultra-dark stays neutral black. Resolves the previous mismatch where AD-Vue 4 hardcoded #001529 / #002140 for the sider, trigger and dark Menu items via Layout.colorBgHeader / colorBgTrigger and Menu's colorItemBg — overrides go through the component-token map now. Round the inbound table's outer corners (header start/end + last row end) and wrap the client expand-row grid in a 1px / 8px-radius border so the list reads as a contained block instead of a flush rectangle. Linter-driven whitespace cleanup across inbounds/*.vue rolled into the same commit since it can't be split out cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): xray tab fixes — modal close, tag validation, full XHTTP, reset to default Modal close: BalancersTab / OutboundsTab / RoutingTab confirmDelete used arrow expressions that returned splice's removed-items array. AD-Vue 4 treats truthy non-thenables from onOk as "still pending" and never closes the dialog (see ActionButton.js:103-106), so the confirm modal stayed open. Wrap the body so onOk returns undefined and AD-Vue auto-closes. Tag validation: outbound + balancer modals only flipped between warning/success on duplicate, leaving the empty case as a green ✓. Split into a 3-state computed — error (empty) / warning (duplicate) / success — and wire a help message so the input clearly explains why the OK button is disabled. Reset to default: re-add the legacy "Reset to Default" panel at the bottom of BasicsTab. Calls /panel/setting/getDefaultJsonConfig and overwrites templateSettings; the existing watch re-stringifies so the JSON tab + dirty-poll see the new state. Restored Basics option lists from main: IPs (4→10, +Vietnam/Spain/ Indonesia/Ukraine/Türkiye/Brazil), DomainsOptions (4→10, +regex entries), BlockDomainsOptions (5→17, +Malware/Phishing/Adult/regex), ServicesOptions (Reddit/Speedtest in, off-template Microsoft out). Outbound form parity with main: • Reverse Sniffing UI for VLESS — toggle + destOverride checkboxes (HTTP/TLS/QUIC/FAKEDNS) + Metadata/Route Only + IPs/Domains excluded multi-selects, gated on reverseTag being set. • Full XHTTP transport — request headers list, Max Upload Size / Min Upload Interval (packet-up), Padding Obfs Mode + sub-fields, Uplink HTTP Method, Session/Sequence/UplinkData placement + keys, No gRPC Header (stream-up/stream-one), expanded XMUX with Max Concurrency/Connections/Reuse/Request/Reusable/Keep-alive. Strip a-divider from the outbound form per request — replaced with plain section/item heading divs so the labels and per-row delete icons stay but the horizontal rule is gone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): xray Advanced tab parity + finalmask gating Advanced tab was a single textarea bound to the full xraySetting blob. Restore the legacy 4-way view: a radio group toggles between All / Inbounds / Outbounds / Routing Rules, and the textarea reads/writes the matching slice through templateSettings. Added the legacy header ("Advanced Xray Configuration Template" + description) so the page introduces itself like main. Outbound finalmask leaked into protocols that don't have a stream (Freedom / Blackhole / DNS / Socks / HTTP / Wireguard) because the v-if only checked outbound.stream. Gate the whole FinalMaskForm on outbound.canEnableStream() to match main. Drop the leading divider inside FinalMaskForm — its parent already provides separation, so the rule above "TCP Masks" was redundant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound Advanced tab live mirror + QR exact-fit sizing Advanced tab in the inbound modal showed stale state. The watch only refreshed advancedJson.stream, so toggling the Sniffing switch in the Sniffing tab left the Advanced JSON showing the prior value. And encryption — stored on inbound.settings.encryption, not on stream — never appeared at all because Advanced only exposed stream + sniffing. Split the watch into three (stream / sniffing / settings) and add a settings textarea so encryption / clients / fallbacks live alongside the existing two views. The submit() path now reads settings from the JSON tab too (falling back to inbound.settings.toString()) so power-user edits in Advanced override the structured form on save. QR canvas: when a longer share-URL bumps the QR matrix size, QRious falls back to floor(canvasSize / matrixWidth) and centers the pattern, leaving a white margin (e.g. matrix=41, size=180 → 8px gap). Pre-pick the QR version from the URL byte length and set canvas size to a multiple of matrixWidth × pixelSize so the pattern always fills it edge-to-edge — no white margin even after toggling encryption on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound stream tidy-up + QR sizing + dev proxy Stream tab clean-up: drop the seven a-divider rules in the inbound form's Stream tab — replace the labelled ones (Request / Response / Security) with a section-heading div that matches the outbound modal, delete the empty rules above TLS sub-blocks / External Proxy / Sockopt. Empty header-list form-items also leaked margin space below each "Add header" button across TCP / WS / HTTPUpgrade / XHTTP — gate each on headers.length > 0 so they vanish until the user adds one. QR panel: drop the link text under the canvas (the user already has a copy button on the header). Pin the canvas display size to a fixed 240px square via :style + image-rendering: pixelated/crisp-edges so a dense WireGuard config QR and its sparser link share the same on-screen footprint without blurring. Dev proxy: Node's AggregateError wraps connection failures whenever DNS returns more than one address (::1 + 127.0.0.1) and the code lands on the inner errors, not the outer. The existing handler only checked err.code so the ECONNREFUSED stack still spammed the log when the Go backend was down. Walk err.errors too, print one friendly line ("backend not reachable — start the Go server"), then stay quiet for the rest of the session. Vendor splitting + chunk-size warning: split node_modules into stable vendor-* chunks so each page only ships the deps it uses and the browser caches them across versions. ant-design-vue stays as a single chunk because its components share internals; raise the chunk-size warning to 1500kB so the build stays quiet (its 1.4MB minified gzips to ~410kB on the wire). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): info-modal cleanup + 2FA QR + outbound link import - 2FA QR: matrix-snap canvas + opaque background to drop white margin - Inbound info modal: stack Mixed/HTTP/Tunnel as info-rows, hide tab strip when only the Inbound tab applies - Add inline VLESS Reverse tag input on first-client form - Hide Protocol tab for TUN (no form yet) - Outbound link converter: route through Outbound.fromLink so vless/trojan/ss/hysteria(2) imports work alongside vmess; fix stray implicit global in fromLink Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): jalali calendar + drop legacy moment-jalali - Wire Calendar Type setting to a real Jalali datepicker via vue3-persian-datetime-picker, gated by useDatepicker composable - DateTimePicker wrapper swaps between AD-Vue and Persian picker; keeps dayjs v-model contract so existing forms/setters work unchanged - Theme picker popup explicitly per body.dark / data-theme=ultra-dark (AD-Vue 4 doesn't expose CSS vars, so var() fallbacks defaulted to white); fix invisible disabled days, SVG arrow fills, popup clipping via append-to="body" - Replace stray moment() calls in dbinbound/inbound models with dayjs; the legacy global was undefined under ESM and broke the inbounds list whenever any inbound had expiryTime > 0 - Remove legacy moment-jalali / persian-datepicker / aPersianDatepicker assets — replaced by the Vue 3 picker Note: dark/ultra background of the date popup still renders white in some cases — pending follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): jalali popup theming + full-month layout - Re-prefix popup selectors with .vpd-wrapper (popup root that travels with appendTo='body'), not .vpd-main (which stays at the input); paints the popup's dark/ultra background again - Drop the 1px border on .vpd-content — with box-sizing: border-box it ate 2px from the day-row width, wrapping the 7th cell of every row and hiding days 18-31 of months that needed a 5th week Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: render dates in Jalali when Calendar Type is jalalian - IntlUtil.formatDate accepts an optional calendar arg; appends the BCP-47 -u-ca-persian extension so Intl renders Jalali across all UI languages, not just fa-IR - Plumb the panel's datepicker setting into the SubPage via the Go injection (window.__SUB_PAGE_DATA__.datepicker) - Panel pages (inbound list/info, client row, xray log) read the same setting through the useDatepicker composable so the whole panel stays consistent Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(frontend): ultra-dark page tint + mobile-friendly inbound view - Drop --bg-page from #21242a (lighter than the cards) to #050505 in ultra-dark across index/sub/settings/inbounds/xray, so cards consistently elevate over the page - Hide the inline sider's children + collapse-trigger and zero its width below 768px; the floating drawer-handle remains the menu trigger - Inbounds page mobile pass: tighten content-area + card padding; flex-wrap the filter bar instead of stacking; shrink table cell padding so all 4 mobile columns fit; bump expand / action / info icon hit targets - Per-client expand row on mobile: soft-tinted rounded cards instead of hairline borders, larger action / info touch targets, more legible email typography, bigger status badge dot Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove legacy template + asset trees and dead Go template engine - Delete web/html/ entirely (page templates, form/, modals/, component/, common/, settings/) — every route is served from web/dist/ now via serveDistPage; nothing in the binary referenced these - Delete web/assets/ entirely (jQuery-era ant-design-vue, axios, moment, codemirror, qrcode/qs/uri/vue/otpauth, custom CSS, Vazirmatn font); Vite bundles all of this into web/dist/assets - Drop the Gin HTML template wiring: remove //go:embed assets + //go:embed html/*, the assetsFS/htmlFS vars, the wrapAssetsFS adapter, EmbeddedHTML / EmbeddedAssets exports, getHtmlFiles / getHtmlTemplate, the i18nWebFunc/funcMap and SetFuncMap call, and the dev/prod template-engine branch — only StaticFS for /assets/ is needed now - Remove dead html()/getContext() helpers and unused imports from web/controller/util.go (no c.HTML(...) callers remain) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(frontend): inbound expand chevron position + cpu history layout - Push the inbound table's expand chevron away from the left edge with margin-inline + cell padding so it isn't flush against the corner - Move "Timeframe: …" caption above the chart (was below); restore the line that the previous edit removed - Fix x-axis time labels being clipped at the bottom of the cpu chart — the offset (paddingTop+drawHeight+22 = 222) exceeded the SVG viewBox height (220); dropped to +14 so labels sit at y=214 with room for descenders - Move the SVG axis text colors out of <style scoped> into a global block — Vue's scoped CSS doesn't always hash-attribute SVG <text> descendants, so the dark-mode overrides via :global() weren't matching; bumped opacity 0.55 → 0.85 for legibility on navy/black Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(login): language picker in settings popover + fluid card sizing - Add language select alongside the theme switch (mirrors SubPage) - Bind headline to pages.login.hello / pages.login.title so the "Hello / Welcome" cycle re-translates with the active locale - Replace AD-Vue 5-breakpoint grid with clamp() sizing so the card scales smoothly instead of jumping ~33% at each breakpoint - Pin horizontal padding so input width stays stable on large viewports Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): organize entry HTML + bootstrap JS into folders - Move entry HTML files: frontend/*.html -> frontend/html/*.html - Move per-page bootstrap modules: src/{index,login,settings,inbounds,xray,subpage}.js -> src/entries/ - Update vite.config rollup inputs and dev-mode MIGRATED_ROUTES to /html/<page>.html - Build output now lands at web/dist/html/<page>.html - serveDistPage and subController updated to read from dist/html/ Cleans up the flat frontend/ root which previously interleaved 6 HTML files with package.json, README, src/, etc. The src/ root similarly gets rid of 6 entry .js files mixed in alongside api/, components/, models/, etc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: remove obsolete vue3 phase1 inventory doc The migration is well past phase 1 — the inventory doc has rotted and the live state lives in the codebase plus the plan files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(frontend): merge utils/legacy.js into utils/index.js The barrel was a placeholder for an eventual split that hasn't happened. Collapsing the two files removes one layer of indirection and the misleading "legacy" name (the contents are still actively used by the migrated SPA). - Move all 930 lines from utils/legacy.js into utils/index.js - Delete utils/legacy.js - Update direct import in models/outbound.js to '@/utils' - Drop a stale legacy.js reference in InboundFormModal comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * revert(frontend): keep entry HTML files at frontend/ root The earlier move to frontend/html/ made dev-mode URLs ugly (http://localhost:5173/html/index.html instead of plain /). The folder didn't add real value — it just hid 6 files behind a non-conventional layout. Reverting that piece while keeping src/entries/ (which is a genuine separation between page bootstrap and the rest of src/). - HTML files back at frontend/<page>.html - Vite rollupOptions.input + MIGRATED_ROUTES restored to flat paths - Build output is web/dist/<page>.html again - web/controller/dist.go and sub/subController.go read from dist/<name> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): bump eslint to 10 + add flat config + clean lint warnings - Upgrade eslint 9.39 -> 10.3 and eslint-plugin-vue 9.33 -> 10.9 - Add eslint.config.js (flat config required by ESLint 10) with vue3-recommended rules, sensible defaults, and exemptions for the project's existing formatting style - Drop --ext from the lint script (removed in ESLint 10) - vue/no-mutating-props is left off because the form-modal pattern ports straight from Vue 2 (parent passes a reactive object, child mutates it); a real fix is an architectural rewire, separate task Lint warning cleanup: - utils/index.js: var -> let/const in the X25519 routines, replace obj.hasOwnProperty(...) with Object.prototype.hasOwnProperty.call(...) - Remove unused imports (reactive, ref, Inbound) in ClientFormModal, InboundInfoModal, QrCodeModal, DnsServerModal, OutboundFormModal, SubPage; remove unused locals (isClientOnline, ONLINE_GRACE_MS, fetchAll, isSocks, isHTTP, _antdAlgorithm) - XrayStatusCard: declare 'open-logs' on defineEmits (was emitted but not declared) - RuleFormModal: rename v-for var t -> tag (shadowed useI18n's t) - Drop stale eslint-disable directives (no-new, no-unused-vars) - OutboundsTab/InboundList: drop redundant initial null assigns - InboundInfoModal/OutboundFormModal: explicit eslint-disable for the intentional local-ref-shadows-prop pattern in modal drafts `npm run lint` now passes with 0 errors and 0 warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): one client identity across multiple inbounds via subId Lets the operator add the same email under the same subId to several inbounds. Xray reports traffic per email, so a single client_traffics row acts as the shared accumulator — no aggregation overhead, quota and expiry stay consistent. - Email validation allows duplicates only when subId matches - AddClientStat upserts via OnConflict DoNothing (idempotent on rerun) - Stat/IP rows survive client deletion when a sibling inbound still references the email - enrichClientStats tops up GORM-preloaded stats with rows whose inbound_id points at a sibling, so every panel view sees usage - disableInvalidClients cascades enable=false and syncs the row's total/expiry into every sibling JSON when the shared identity expires - DelDepletedClients removes the depleted client from all referencing inbounds, batched - Subscription services dedupe traffic by email so shared quota is counted once Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(frontend): rewrite README for multi-page Vue 3 layout Reflects the current state — embedded build, per-route HTML entries, ESLint 10 flat config, src/ layout, and the steps to add a new page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * build(frontend): drop deprecated rimraf/glob/inflight transitive deps vue3-persian-datetime-picker pinned moment-jalaali to ^0.9.4, which pulled rimraf@3 → glob@7 → inflight@1. inflight in particular leaks memory and is unmaintained. Override moment-jalaali to ^0.10.4 (same runtime API, dropped the legacy build deps) so npm install no longer warns and the dep tree is 12 packages lighter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(nodes): multi-node panel orchestration (CRUD, deployment, traffic sync, sub per-node) - Node model + service + controller (/panel/api/nodes/*) with bearer-token apiToken auth - Heartbeat job @every 10s; status/latency/xrayVersion surfaced in Nodes UI - Runtime abstraction (Local + Remote) so inbound/client mutations target the inbound's owning node instead of always hitting the local xray - Inbounds gain optional NodeID; tag-based correlation with remote panel (no RemoteInboundID column needed) - NodeTrafficSyncJob @every 10s pulls absolute counters + online/lastOnline from each enabled+online node and writes them into central DB; 30s reset grace window prevents post-reset overwrite - Reset propagation to nodes (best-effort) on client/inbound/all reset paths - Subscription server uses node.Address for inbounds with NodeID, falling back to existing host resolution for local inbounds - Frontend: Nodes page, "Deploy to" select in inbound form, Node column on inbound list, hostOverride threaded through genAllLinks/QR/Info modals Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(stats): system history modal + per-node CPU/Mem trends across all locales Backend - web/service/metric_history.go: generic in-memory ring buffer with two singletons — system-wide (cpu/mem/netUp/netDown/online/load1/5/15) and per-node (cpu/mem) keyed by node id - ServerService.AppendStatusSample writes all 8 metrics every 2s on the same tick; AppendCpuSample/AggregateCpuHistory kept for back-compat - NodeService.UpdateHeartbeat appends cpu/mem only on online ticks so offline gaps render as missing data, not phantom dips - New routes: GET /panel/api/server/history/:metric/:bucket and GET /panel/api/nodes/history/:id/:metric/:bucket, both whitelisted Frontend - Sparkline component generalized: arbitrary value range (auto-scale when valueMax=null), pluggable yFormatter/tooltipFormatter for B/s, client counts, load averages - SystemHistoryModal replaces CpuHistoryModal with tabs for every metric; opened from a tag on the 3X-UI card next to Documentation - NodeHistoryPanel: expandable row on the Nodes table showing per-node CPU and Mem trends, refreshed every 15s Localization - Backfill systemHistoryTitle / trendLast2Min / pages.inbounds.{node, deployTo, localPanel} and the entire pages.nodes block (51 keys including statusValues + toasts) into all 11 non-en/fa locales: ar-EG, es-ES, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(embed): include underscore-prefixed Vite chunks in dist FS go:embed silently excludes files whose names start with `_` or `.`, so the `_plugin-vue_export-helper-<hash>.js` chunk that Vite/rolldown emits for @vitejs/plugin-vue was missing from the production binary. First import at runtime hit a 404 and the SPA failed to mount — blank page on every page load, no error in the server logs because the asset 404 was just a static-handler miss. Switched the directive to `//go:embed all:dist` which keeps the same root layout but disables the underscore/dot exclusion rule. Dev mode was unaffected (it serves dist/assets/ from disk, not the embedded FS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * ci: build frontend bundle before Go compile in release.yml + Dockerfile Phase 8 cut all panel HTML routes over to web/dist/ and embedded the Vite bundle into the Go binary via //go:embed all:dist. web/dist/ is .gitignored, so on a fresh CI checkout it doesn't exist — every Go build since Phase 8 has been failing with "pattern dist: no matching files found" or producing a binary that 404s on first asset request. release.yml: add a setup-node@v4 + npm ci + npm run build trio before the existing go build step in both the Linux matrix job (7 arches) and the Windows job. npm cache is keyed on frontend/package-lock.json. Dockerfile: add a node:22-alpine frontend stage that runs npm ci + npm run build and emits to /src/web/dist (via vite.config.js's outDir). The golang builder stage then COPY --from=frontend /src/web/dist into ./web/dist before the go build, so embed.FS sees the bundle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ws): live updates on inbounds/xray/nodes pages, drop polling + manual refresh Replaces the legacy polling + manual-refresh model with WebSocket pushes across the three live-data pages. The hub already broadcast traffic / client_stats / outbounds; this wires the frontend to consume them and adds a new `nodes` channel for the heartbeat job's snapshot. Frontend - new useWebSocket composable: page-scoped singleton WebSocketClient, lifecycle-managed on/off, leaves disconnect to page-unload - inbounds: useInbounds gains applyTrafficEvent / applyClientStatsEvent / applyInvalidate that merge counters and online/lastOnline in place; InboundsPage subscribes; InboundList drops the auto-refresh popover, the refresh button, and the now-unused refreshing prop - xray outbounds: useXraySetting gains applyOutboundsEvent; XrayPage subscribes; OutboundsTab drops the refresh button + emit - nodes: useNodes gains applyNodesEvent and stops the 5s setInterval/visibilitychange polling; NodesPage subscribes; NodeList drops the refresh button and ReloadOutlined import Backend - web/websocket: new MessageTypeNodes + BroadcastNodes notifier - node_heartbeat_job: after wg.Wait(), reload the table once and BroadcastNodes(updated). Gated on websocket.HasClients() so a panel with no open browser doesn't spend the DB read Bug fixes spotted in this pass - websocket.js #buildUrl defaulted basePath to '' when the global was missing (dev mode), producing `ws://host:portws` and a SyntaxError on the WebSocket constructor. Fall back to '/' and ensure leading slash. - vite.config.js: forward /ws to ws://localhost:2053 with ws:true so dev (5173) reaches the Go backend's WebSocket - NodeFormModal: a-input-password's visibilityToggle is Boolean in AntD Vue 4; the v3-era object form (`{ visible, 'onUpdate:visible' }`) triggered a Vue prop-type warning. Drop the override (default true shows the eye icon and toggles internally) and remove the orphaned tokenVisible ref Translations - pages.inbounds.autoRefresh / autoRefreshInterval: removed from all 13 locales (UI gone) - pages.nodes.refresh: removed from all 13 locales (UI gone) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(inbounds): hide Node column when no nodes are defined Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:38:48 +00:00
"ResetAllTraffics": "すべてのトラフィックをリセット",
"SortedTrafficUsageReport": "ソートされたトラフィック使用レポート"
},
"answers": {
"successfulOperation": "✅ 成功!",
"errorOperation": "❗ 操作エラー。",
"getInboundsFailed": "❌ インバウンド情報の取得に失敗しました。",
"getClientsFailed": "❌ クライアントの取得に失敗しました。",
"canceled": "❌ {{ .Email }}:操作がキャンセルされました。",
"clientRefreshSuccess": "✅ {{ .Email }}:クライアントが正常に更新されました。",
"IpRefreshSuccess": "✅ {{ .Email }}IPが正常に更新されました。",
"TGIdRefreshSuccess": "✅ {{ .Email }}クライアントのTelegramユーザーが正常に更新されました。",
"resetTrafficSuccess": "✅ {{ .Email }}:トラフィックが正常にリセットされました。",
"setTrafficLimitSuccess": "✅ {{ .Email }}:トラフィック制限が正常に保存されました。",
"expireResetSuccess": "✅ {{ .Email }}:有効期限の日数が正常にリセットされました。",
"resetIpSuccess": "✅ {{ .Email }}IP制限数が正常に保存されました{{ .Count }}。",
"clearIpSuccess": "✅ {{ .Email }}IPが正常にクリアされました。",
"getIpLog": "✅ {{ .Email }}IPログの取得。",
"getUserInfo": "✅ {{ .Email }}Telegramユーザー情報の取得。",
"removedTGUserSuccess": "✅ {{ .Email }}Telegramユーザーが正常に削除されました。",
"enableSuccess": "✅ {{ .Email }}:正常に有効化されました。",
"disableSuccess": "✅ {{ .Email }}:正常に無効化されました。",
"askToAddUserId": "設定が見つかりませんでした!\r\n管理者に問い合わせて、設定にTelegramユーザーのChatIDを使用してください。\r\n\r\nあなたのユーザーChatID<code>{{ .TgUserID }}</code>",
"chooseClient": "インバウンド {{ .Inbound }} のクライアントを選択",
"chooseInbound": "インバウンドを選択"
}
}
}