- 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>
- 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>
- 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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>