| `{{ x \| filter }}` | **0** | 0 | ✅ none | Filters removed in Vue 3 — but we don't use any. |
| `<template slot="X">` | 233 | 36 | medium | Rewrite to `<template #X>`. Mechanical. |
| `slot-scope="X"` (incl. above) | 275 total | 40 | medium | Rewrite to `v-slot:name="X"`. Mechanical. |
| `scopedSlots: { ... }` (in JS column defs) | 49 | 4 | medium | Vue 3 removed `scopedSlots`. All slots are now scoped. Replace with `slots: { ... }` or rely on template slot binding only. |
| `new Vue({...})` mounts | ~30+ | 30 | medium | Replace with `createApp({...}).mount('#id')`. Each page mounts its own Vue instance. |
| `.sync` modifier | 0 | 0 | ✅ none | Removed in Vue 3, replaced by `v-model:propName`. We don't use it. |
| `v-model` | 358 | 36 | **high** | Default `v-model` on custom components changed (`value` → `modelValue`, `input` event → `update:modelValue`). Most of these target AD-Vue components, which AD-Vue 4 already adapts to internally — but components we wrote ourselves (e.g. `aClientTable`, `aTableSortable`) need updates. |
| `Vue.set` / `Vue.delete` / `Vue.observable` | 0 | 0 | ✅ none | Replaced by `reactive()`. Not needed. |
| `transition` class names (`-enter`, `-leave`) | 1 (in `qrcode_modal.html`) | 1 | low | Renamed in Vue 3: `*-enter` → `*-enter-from`, `*-leave-to` stays, etc. |
| Key modifiers using `keyCode` (`@keyup.13`) | 0 | 0 | ✅ none | Number key codes removed in Vue 3. We don't use them. |
| `ref="..."` attrs | 21 | 9 | low | Behavior unchanged for Options API — refs still work the same. |
| `.native` event modifier | 0 | 0 | ✅ none | Removed in Vue 3 (events on components are no longer "fake" by default). |
## Ant Design Vue 1.x → 4.x breakage surface
Total `<a-*>` component instances: **3,145 across 63 files**. This is the bulk of the migration cost.
The most-used components (rough estimate from grep):
-`a-form` + `a-form-item` — Form API was substantially redesigned in AD-Vue 3+. We use it lightly (`layout="vertical"`, `label-col`, `wrapper-col`); migration is mechanical but every form needs touching.
-`a-table` — column definitions with `scopedSlots` (49 instances) need to become `slots: { customRender: 'name' }` or use template slot binding directly.
-`a-modal` — **`v-model` on a-modal changed**: `v-model="visible"` → `v-model:open="visible"` (or `v-model:visible` depending on version). Every modal needs updating.
-`a-icon` — **removed as a generic component in AD-Vue 4.** Each icon must be imported individually from `@ant-design/icons-vue`. We use ~233 `a-icon` references — mostly via `type="..."` attribute. Likely the single highest-friction change.
-`aSettingListItem.html`, `aCustomStatistic.html` — small wrappers
- These are the **only places using the deprecated event-bus APIs** (4 occurrences).
## Server-side coupling
The Go layer interpolates translations directly into templates via `{{ i18n "key" }}`. After migration we have two options:
- **Keep Go-side i18n.** Vite builds .html partials that still get processed by Go's `template` package. Means `<script type="module">` entrypoints reference build artifacts but markup is still server-rendered. Pro: smallest change. Con: every page change forces a Vite rebuild *and* a Go restart.
- **Move i18n to client side.** Export the translation TOML files as JSON, ship as static assets, use `vue-i18n`. Pro: cleaner client/server split, hot reload during dev. Con: more change, every i18n key reference in templates must be rewritten.
We will defer this decision to Phase 7. For Phases 2–4 we keep the Go-side approach.
## Risk-ranked migration order
Order chosen so that breakage is contained and we always have a working panel:
1.**Phase 2 — Toolchain.** Vite scaffold; Go binary embeds `dist/` via `embed.FS`. New build runs in CI alongside existing static assets; legacy continues to work.
2.**Phase 3 — Utils.** Migrate framework-agnostic JS first. Zero Vue dependency, zero risk.
3.**Phase 4 — `login.html`.** Smallest page with state. Hits every Vue 2→3 syntax change and every AD-Vue 1→4 component change at small scale. Becomes the template the rest follow.
4.**Phase 5 — Medium pages and modals.**`index.html`, `settings.html`, all modals. ~30 templates of 200-1000 lines each.
5.**Phase 6 — `xray.html`.** The 2,360-line page with the inbound/outbound editors. Highest regression risk — will likely break and need fixing in the QA pass.
-`aThemeSwitch.html` had two near-identical components (full menu version + login popover version) plus a `themeSwitcher` global object. Refactored into:
-`composables/useTheme.js` — single reactive `theme` state, `toggleTheme/toggleUltra`, and a `pauseAnimationsUntilLeave` helper. Boot side-effects (apply stored theme + persist on change) run via `watchEffect`. Importing the module is enough to apply the right theme before mount.
-`components/ThemeSwitch.vue` — full menu version (used in the main panel sidebar).
- AD-Vue 4 dropped `<a-icon>`. The `BulbFilled` / `BulbOutlined` swap is done via `<component :is="BulbIcon">`.
-`Vue.component('a-theme-switch', { ... })` global registration → per-page imports.
-`this.$message.config(...)` (Vue 2 instance method) → `message.config(...)` from `ant-design-vue`, called once at app boot in `login.js`.
- vue-i18n bumped 10 → 11.1.4 (npm warned that v9/v10 are EOL).
- Vite 8.0.11 build verified — `npm run build` succeeds, outputs `web/dist/login.html` + chunked JS/CSS. AD-Vue with `app.use(Antd)` produces a 1.5 MB chunk; we'll switch to per-component imports in a later cleanup pass.
**Known gap:** the legacy `web/assets/css/custom.min.css` styles `body.dark` / `body.light` / `[data-theme="ultra-dark"]`. The new login page doesn't import that CSS, so toggling theme switches AD-Vue's own components but not the panel chrome (e.g. card backgrounds). The composable still toggles the body class so behavior is correct — visual fidelity is restored when we either port custom.css to the new build or import it directly.
Migrated four components into `frontend/src/components/`:
- **`CustomStatistic.vue`** — wraps `<a-statistic>` with prefix/suffix slots. Trivial port; only change is `Vue.component(...)` → SFC.
- **`SettingListItem.vue`** — wraps `<a-list-item>` with title/description/control slots and a `paddings` prop. Trivial port.
- **`AppSidebar.vue`** — main panel sidebar. Surfaces tabs (Dashboard / Inbounds / Settings / Xray / Logout) with icons and the theme switcher. Two key changes from the legacy:
- AD-Vue 4 dropped `<a-icon :type="dynamic">`. Replaced with a name→component map (`{ dashboard: DashboardOutlined, ... }`) rendered via `<component :is="...">`.
-`<a-drawer>``slot="handle"` (a non-standard prop in legacy AD-Vue 1) was replaced with a fixed-position toggle button rendered as a sibling. The drawer's `:visible` was renamed to `:open` per AD-Vue 4 conventions.
- Tab `key` paths and `requestUri` are passed in as props (parent page knows the basePath); the legacy embedded `{{ .base_path }}` directly via Go templating.
- **`TableSortable.vue`** — drag-to-reorder a-table wrapper. The biggest single Vue 3 / AD-Vue 4 rewrite in this phase:
-`$listeners` (Vue 2) is gone — replaced by `inheritAttrs: false` + `v-bind="$attrs"` style forwarding via the `attrs` setup return.
-`scopedSlots: this.$scopedSlots` → Vue 3 unifies all slots; just iterate `Object.keys(this.slots)` and forward.
- Render-function shape changed: Vue 2's `h(tag, { props, on, scopedSlots }, children)` → Vue 3's `h(tag, { ...props, ...on }, slotsObject)` where slot fns are passed as the third arg.
-`'a-table'` as a plain string → `resolveComponent('a-table')` so `app.use(Antd)` registration is honored.
-`inject: ['sortable']` (Options API) inside child component swapped for `inject('sortable', null)` (Composition API) since the trigger now uses `setup()`.
-`customRow`'s return shape flattened: Vue 2 nested `attrs/on/class` is now a flat object of attrs + listeners + class.
**Skipped in 5b:**
- **`aClientTable.html`** — *not actually a component*. It's a fragment of `<template slot="X" slot-scope="...">` blocks pulled into pages that use it. It depends on outer-scope variables (`record`, `app`, `themeSwitcher`, etc.) and only makes sense inlined into its consumer. Will migrate as part of `inbounds.html`.
- **`aPersianDatepicker.html`** — wraps a Persian-only third-party datepicker that isn't in the critical path. Defer until `settings.html` migrates and we know whether to keep the legacy lib, replace with a Vue 3 wrapper, or fall back to a native HTML5 date input.
- Vite 6 → Vite 8.0.11 (released March 2026). Requires Node 20.19+ or 22.12+. `@vitejs/plugin-vue` bumped to ^6.0.6 which lists vite ^8 as a peer.
- Multi-page Vite: each migrated page = its own entry. `frontend/login.html` registered alongside `frontend/index.html` in `rollupOptions.input`. Same approach for the rest of the migration.
- New page lives at `frontend/src/pages/login/LoginPage.vue` with a thin entrypoint at `frontend/src/login.js` that calls `setupAxios()`, installs Antd, and mounts.
- **Three legacy features deferred** to keep Phase 4 small:
- **i18n** — strings hardcoded in English. Phase 7 wires up vue-i18n with the existing TOML files.
- **Theme switcher** — `<a-theme-switch-login>` was a custom component referencing a global `themeSwitcher`. Deferred to Phase 5 with the rest of the shared components.
- **Headline word-cycling animation** — purely aesthetic, not migrated.
- **Password-manager DOM observer** — the `pm_*` script that strips inline styles from autofilled inputs. Niche workaround, easy to add back if needed.
-`<a-icon slot="prefix" type="user">` → `<template #prefix><UserOutlined /></template>` with explicit imports from `@ant-design/icons-vue`. **AD-Vue 4 removed the generic `<a-icon>` — every icon must be imported individually.**
-`v-model.trim` on `a-input` → `v-model:value` (AD-Vue 4 uses named v-model on inputs). The `.trim` modifier is dropped; trim manually if needed.
-`new Vue({ el: '#app', delimiters, data, methods })` → `createApp(LoginPage).use(Antd).mount('#app')` with `<script setup>` and Composition API.
-`mounted()` → `onMounted()`.
-`el: '#app', delimiters: ['[[', ']]']` is gone — SFCs use the standard `{{ }}`.
-`web/assets/js/util/index.js` (927 lines, 21 classes) → `frontend/src/utils/legacy.js`. All classes prefixed with `export`. Barrel `frontend/src/utils/index.js` re-exports for cleaner consumer imports.
-`Vue.prototype.$message[...]` inside `HttpUtil._handleMsg` was replaced with a direct `import { message } from 'ant-design-vue'`. Vue 3 has no `Vue.prototype`.
-`RandomUtil.randomShadowsocksPassword` previously defaulted to `SSMethods.BLAKE3_AES_256_GCM` from inbound.js, which would create a circular import. Replaced with the literal string default `'2022-blake3-aes-256-gcm'`.
-`MediaQueryMixin` removed from utils. Replaced by `frontend/src/composables/useMediaQuery.js`, a Vue 3 composable returning a reactive `isMobile`.
-`web/assets/js/axios-init.js` was an imperative side-effect script. Wrapped as `setupAxios()` which the app calls once at startup. `Qs` global → `import qs from 'qs'`.
-`web/assets/js/websocket.js` exported as `WebSocketClient`. The bottom `window.wsClient = ...` line was removed; pages instantiate the client themselves with the basePath they need.
- Models (`inbound`, `outbound`, `dbinbound`, `setting`, `reality_targets`) copied verbatim with `export` added and the imports they need from utils/legacy.js wired up.
-`subscription.js` deferred to Phase 5 — it's a Vue 2 mount, not a util.
- Smoke test added to `frontend/src/App.vue` exercising `SizeFormatter`, `RandomUtil`, `Wireguard`, and `useMediaQuery`. If `npm run dev` renders it correctly, Phase 3 is verified.