The legacy panel CSS (custom.min.css ported as legacy.css) tinted every
non-primary button teal-green via .dark .ant-btn:not(.ant-btn-primary)
overrides while AD-Vue 4's darkAlgorithm kept primary buttons blue —
producing the mixed blue/green button look on dark mode. Drop legacy.css
entirely and let AD-Vue 4's algorithms own the palette.
Centralize antdThemeConfig in useTheme.js so every page resolves to the
same source of truth (light = defaultAlgorithm, dark = darkAlgorithm,
ultra-dark = darkAlgorithm + deeper colorBgBase/Layout/Container/
Elevated tokens). Each page's <a-config-provider> now imports the
shared computed instead of defining its own copy.
Drops the 67 KB legacy CSS chunk; per-page CSS bundles fall to ≤5.9 KB.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Index dashboard regains the 8 cards that were lost in the SPA port
(3X-UI panel info, Operation Hours, System Load, Usage, Overall Speed,
Total Data, IP Addresses, Connection Stats), plus a Config button that
shows the live xray config.json. Version display falls back through
panelUpdateInfo → window.__X_UI_CUR_VER__ → '?' so dev mode isn't blank.
- Xray config no longer hangs on load: useXraySetting surfaces failures
instead of leaving a perpetual spinner, and the Vite dev proxy stops
hijacking POST requests to migrated routes (only GETs get bypassed).
- Inbound page no longer throws __asyncLoader/emitsOptions errors —
inbound.js was missing imports (NumberFormatter, SizeFormatter,
Wireguard) and InboundList kept emitting after unmount.
- Login round-trip works after logout: a public /csrf-token endpoint
bootstraps the SPA before authentication, axios caches the token
module-level, and the dev 401 handler navigates to /login.html
instead of reloading the dashboard into a redirect loop.
- legacy.css mirrors the legacy panel's surface/text variables so dark
and ultra-dark themes match main; every SPA entry imports it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>